
参考:
1. 项目搭建
小布旅购助手 项目搭建:全局配置和一级路由配置,底部导航对应页面 title 配置。
① 通过 js 基础模版创建项目,选择自己注册好的小程序。
② 删除 utils 目录、清空 pages 目录
③ 清空 app.js
1 2 3 4 5
| App({ onLaunch() { }, })
|
④ 定义底部选项卡,通过 app.json - pages
1 2 3 4 5 6
| "pages": [ "pages/home/home", "pages/category/category", "pages/shopcar/shopcar", "pages/center/center" ],
|
⑤ 配置顶部颜色和信息,通过 app.json -window
1 2 3 4 5 6
| "window": { "navigationBarTextStyle": "white", "navigationBarTitleText": "小布旅购助手", "navigationBarBackgroundColor": "#14c145", "backgroundTextStyle": "light" },
|
⑥ 配置底部选项卡高亮图片切换(阿里iconfont矢量图标),通过 app.json - tabBar
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| "tabBar": { "list": [ { "pagePath": "pages/home/home", "text": "首页", "iconPath": "images/home.png", "selectedIconPath": "images/home_light.png" }, { "pagePath": "pages/category/category", "text": "分类", "iconPath": "images/category.png", "selectedIconPath": "images/category_light.png" }, { "pagePath": "pages/shopcar/shopcar", "text": "购物车", "iconPath": "images/shopcar.png", "selectedIconPath": "images/shopcar_light.png" }, { "pagePath": "pages/center/center", "text": "我的", "iconPath": "images/center.png", "selectedIconPath": "images/center_light.png" } ] },
|

2. restful 接口
json-server 通过 json 文件 mock 数据,模拟 restful 接口。
参考文档:https://rtool.cn/jsonserver/docs/introduction
模拟数据:json-server -w db.json -p 5000
请求地址:http://localhost:5000/xxx
3. request 封装+ loading
request.js 封装 Promise 风格请求方式工具,可以很方便链式调用。
import request from '[path]/utils/request' JS 模块导入方式
wx.showLoading | wx.hideLoading 显示 或 隐藏请求的 loading 框
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
var BASE_URL = "http://localhost:5000" function request(params) { wx.showLoading({ title: '正在加载中' }) return new Promise((resolve, reject) => { wx.request({ ...params, url: BASE_URL + params.url, success: (res) => { resolve(res.data) }, fail: (err) => { reject(err) }, complete: () => { wx.hideLoading({ success: (res) => { } }) } }) }) }
module.exports = request
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import request from '../../utils/request'
Page({ data: {}, handleGetTap() { request({ url: "/users" }).then((res) => { console.log(res); }) }, handlePostTap() { request({ url: '/users' }).then(res => { console.log(res); }).catch(err => { console.error(err); }) }, ... })
|

4. 首页
【使用扩展组件】
初始化 npm:npm init - 初始化生成 package.json
安装 sticky:npm i @miniprogram-component-plus/sticky
构建 npm:【工具】-【构建npm】 (否则引入会报错)
引入 sticky:https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/extended/component-plus/sticky.html
-> 375宽度的机型上不生效,原因未知,可能是组件库BUG(验证过非边缘像素计算临界值问题)
微信团队扩展组件:https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/extended/component-plus/
轮播
首页轮播:优化request封装同时暴露基地址、轮播组件中拼接基地址、基地址挂载到全局对象的方式和使用
utils/request.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| const BASE_URL = "http://localhost:5000"; function request(params) { wx.showLoading({ title: '正在加载中' }) return new Promise((resolve, reject) => { wx.request({ ...params, url: BASE_URL + params.url, success: (res) => { resolve(res.data) }, fail: (err) => { reject(err) }, complete: () => { wx.hideLoading({ success: (res) => { } }) } }) }) }
module.exports = { request, BASE_URL };
|
app.js
1 2 3 4 5 6 7 8 9 10 11
| import { BASE_URL } from './utils/request'
App({ onLaunch() { this.globalData = { BASE_URL: BASE_URL } }, })
|
pages/home/home.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| import {request} from '../../utils/request'
Page({
data: { BASE_URL: '', looplist: [] },
onLoad(options) { this.setData({ BASE_URL: getApp().globalData.BASE_URL }); this.renderSwiper() },
renderSwiper() { request({ url: '/recommends' }).then(res => { console.log(res); this.setData({ looplist: res }) }).catch(err => { console.error(err); }) }, ... })
|
1 2 3 4 5 6
| <swiper indicator-dots="{{true}}" circular="{{true}}" autoplay="{{true}}" interval="2000"> <swiper-item wx:for="{{looplist}}" wx:key="index"> <image src="{{BASE_URL + item.url}}" mode="widthFix"/> </swiper-item> </swiper>
|

列表+懒加载
首页列表:商品列表请求和渲染展示、列表滚动到底懒加载处理、下拉刷新逻辑
1 2 3 4 5 6 7
| <view wx:for="{{goodlist}}" wx:key="index" class="goodbox"> <image src="{{BASE_URL + item.poster}}" mode="widthFix" /> <view>{{item.title}}</view> <view>价格:<text style="color: red">¥{{item.price}}</text></view> <view>好评率:{{item.goodcomment}}</view> </view>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| import {request} from '../../utils/request'
Page({
data: { BASE_URL: '', looplist: [], goodlist: [], currentPage: 1, goodTotal: 0 },
onLoad(options) { this.setData({ BASE_URL: getApp().globalData.BASE_URL }); this.renderGoods() },
renderGoods() { request({ url: `/goods?_page=${this.data.currentPage}` }).then(res => { console.log(res.data); this.setData({ goodlist: [...this.data.goodlist, ...res.data], goodTotal: res.items }) }).catch(err => { console.error(err); }) },
onReachBottom() { console.log(this.data.goodlist.length, this.data.goodTotal); if (this.data.goodlist?.length === this.data.goodTotal) { console.log("滚动到底了..."); return } this.setData({ currentPage: this.data.currentPage + 1 }) this.renderGoods() }, ... })
|
下拉刷新更新数据:
1 2 3 4 5
| { "usingComponents": {}, "navigationBarTitleText": "首页", "enablePullDownRefresh": true }
|
1 2 3 4 5 6 7 8 9 10 11
|
onPullDownRefresh() { setTimeout(() => { console.log("下拉更新数据了"); wx.stopPullDownRefresh() }, 1000) },
|
搜索+吸顶
搜索和吸顶:自定义组件搜索输入框,吸顶引入扩展组件 sticky 使用
components/search/search.xx
1 2
| <input placeholder="请输入搜索内容" bindtap="handleTap" />
|
1 2 3 4 5 6 7 8
| Component({ methods: { handleTap() { this.triggerEvent("SearchEvent") } } })
|
1 2 3 4 5 6 7 8 9
| input{ border: 1rpx solid gray; border-radius: 10rpx; margin: 10rpx; padding: 10rpx; height: 30rpx; background-color: white; }
|
pages/home/home.xx
1 2 3 4 5 6 7 8
| { "usingComponents": { "mysearch": "../../components/search/search", "mp-sticky": "@miniprogram-component-plus/sticky" }, "navigationBarTitleText": "首页", "enablePullDownRefresh": true }
|
1 2 3 4 5 6 7
|
<mp-sticky offset-top="0"> <view style="width: 100vw"> <mysearch bindSearchEvent="handleSearchEvent"></mysearch> </view> </mp-sticky>
|

页面跳转+带参
页面跳转带参:商品列表页绑定点击事件,携带多个参数到详情页面,详情页解析参数并设置自己的导航栏标题
wx.navigateTo() 直接跳转到【目标页】
wx.redirectTo() 关闭当前页面再跳转到目标页
wx.switchTab() 跳转【底部选项卡】页
wx.setNavigationBarTitle({ title: '' }) 设置当前页顶部导航栏的标题
pages/home/home.xx
1 2 3 4 5
|
<view wx:for="{{goodlist}}" wx:key="index" class="goodbox" bindtap="handleGoToDetail" data-id="{{item.id}}" data-title="{{item.title}}"> ... </view>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import { request } from '../../utils/request'
Page({ handleGoToDetail(evt) { var id = evt.currentTarget.dataset.id var title = evt.currentTarget.dataset.title console.log(id, title); wx.navigateTo({ url: `/pages/detail/detail?id=${id}&title=${title}`, }) }, ... })
|
app.json
1 2 3 4 5 6 7
| { "pages": [ "pages/home/home", ... "pages/detail/detail" ], }
|
pages/detail/detail.xx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| Page({
onLoad(options) { console.log(options); wx.setNavigationBarTitle({ title: options.title, }) }, ... })
|
5. 详情页
【添加编译模式】
设置后,后面就只编译这一页,对开发进行时相当友好。

轮播+全屏预览
轮播和全屏预览:详情页轮播图、原生方法全屏预览轮播图
wx.previewImage({ }) 全屏预览图片
1 2 3 4 5 6 7 8
|
<swiper indicator-dots="{{true}}" circular="{{true}}" autoplay="{{true}}" interval="2000"> <swiper-item wx:for="{{info.slides}}" wx:key="index"> <image src="{{BASE_URL + item}}" mode="aspectFit" bindtap="handleFullScreenTap" data-current="{{BASE_URL + item}}" /> </swiper-item> </swiper>
|
1 2 3 4 5 6 7 8
| swiper image{ width: 100%; height: 200px; } swiper { height: 200px; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| const { request, BASE_URL } = require("../../utils/request");
Page({ data: { BASE_URL: '', info: null }, onLoad(options) { this.setData({ BASE_URL: getApp().globalData.BASE_URL }); console.log(options); wx.setNavigationBarTitle({ title: options.title, }) this.getGoodDetailById(options.id) },
getGoodDetailById(id) { request({ url: `/goods/${id}` }).then(res => { console.log(res); this.setData({ info: res }) }).catch(err => { console.error(err); }) },
handleFullScreenTap(evt) { wx.previewImage({ current: evt.currentTarget.dataset.current, urls: this.data.info.slides.map(item => BASE_URL + `${item}`) }) }, ... })
|

详情
商品详情:页内导航和高亮处理、吸顶效果、商品详情遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
|
<swiper indicator-dots="{{true}}" circular="{{true}}" autoplay="{{true}}" interval="2000"> <swiper-item wx:for="{{info.slides}}" wx:key="index"> <image src="{{BASE_URL + item}}" mode="aspectFit" bindtap="handleFullScreenTap" data-current="{{BASE_URL + item}}" /> </swiper-item> </swiper>
<mp-sticky offset-top="0"> <view class="detailtabbar" style="width: 100vw;"> <view class="{{current === 0 ? 'active' : ''}}" bindtap="handleActive" data-index="{{0}}">商品详情</view> <view class="{{current === 1 ? 'active' : ''}}" bindtap="handleActive" data-index="{{1}}">用户评价</view> </view> </mp-sticky>
<view wx:if="{{current === 0}}"> <view style="color: gray; margin: 10px;">{{info.feature}}</view> <image wx:for="{{info.desc}}" src="{{item}}" mode="widthFix" style="width: 100%;" /> </view>
<view wx:else> <view wx:for="{{comments}}" wx:key="index" style="border-bottom: 1px solid lightgray;"> <view class="user"> <image src="{{BASE_URL + item.userImageUrl}}" mode="widthFix" class="left" /> <view class="left">{{item.nickname}}</view> <view class="right">{{item.creationTime}}</view> </view> <view class="content">{{item.content}}</view> <view class="content"> <image src="{{BASE_URL + item.imgUrl}}" mode="widthFix" /> </view> </view> </view>
<view class="bottom"> <view style="background-color: #14c145;">查看购物车</view> <view style="background-color: #F76260;">加入购物车</view> <view style="background-color: #ffa591;">立即购买</view> </view>
|
1 2 3 4 5
| { "usingComponents": { "mp-sticky": "@miniprogram-component-plus/sticky" } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| swiper image { width: 100%; height: 200px; } swiper { height: 200px; }
.detailtabbar { display: flex; flex-direction: row; text-align: center; height: 100rpx; line-height: 100rpx; background-color: white; }
.detailtabbar view { flex: 1; } .detailtabbar .active { border-bottom: 1px solid red; }
.user { overflow: hidden; padding: 20px; } .user .left { float: left; height: 100rpx; line-height: 100rpx; } .user image { width: 100rpx; border-radius: 50rpx; } .user .right { float: right; height: 100rpx; line-height: 100rpx; } .content { padding: 20rpx; } .content image { width: 300rpx; }
.bottom { height: 100rpx; line-height: 100rpx; text-align: center; position: fixed; left: 0; bottom: 0; display: flex; flex-direction: row; justify-content: space-around; width: 100%; } .bottom view{ flex: 1; color: white; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
| const { request, BASE_URL } = require("../../utils/request");
Page({
data: { BASE_URL: '', info: null, current: 0, comments: [] },
onLoad(options) { this.setData({ BASE_URL: getApp().globalData.BASE_URL }); console.log(options); wx.setNavigationBarTitle({ title: options.title, }) this.getGoodDetailById(options.id) this.getGoodComment() },
getGoodDetailById(id) { request({ url: `/goods/${id}` }).then(res => { console.log(res); this.setData({ info: res }) }).catch(err => { console.error(err); }) },
handleFullScreenTap(evt) { wx.previewImage({ current: evt.currentTarget.dataset.current, urls: this.data.info.slides.map(item => BASE_URL + `${item}`) }) },
handleActive(evt) { this.setData({ current: evt.currentTarget.dataset.index }) },
getGoodComment() { request({ url: "/comments" }).then(res => { console.log(res); this.setData({ comments: res }) }).catch(err => { console.log(err); }) }, ... })
|

评价
商品用户评价:评价内容请求、列表布局、底部固定3个按钮布局
代码同上。

6. 搜索模块
【使用 WeUI】
官方文档:https://wechat-miniprogram.github.io/weui/docs/
- 通过 useExtendedLib 扩展库 的方式引入,这种方式引入的组件将
不会计入代码包大小。
- 可以通过npm方式下载构建,npm包名为
weui-miniprogram
app.json - 使用第 1 种方式引入,只需要添加一个配置即可
1 2 3 4 5 6
| { ... "useExtendedLib": { "weui": true } }
|
搜索框 SearchBar
参考文档:https://wechat-miniprogram.github.io/weui/docs/search.html
pages/search/search.xx
1 2 3 4 5 6
| { "usingComponents": { "mp-searchbar": "weui-miniprogram/searchbar/searchbar" }, "navigationBarTitleText": "搜索" }
|
1 2 3
|
<mp-searchbar bindselectresult="handleSearchResult" search="{{search}}"></mp-searchbar>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| const { request } = require("../../utils/request");
Page({ data: { search: '' }, onLoad(options) { this.setData({ search: this.search.bind(this) }) }, search(value) { return Promise.all([ request({ url: `/goods` }), request({ url: `/categories` }) ]).then(res => { console.log(res[0], res[1]); var goodsTitles = res[0].filter(item => item.title.includes(value)).map(item => { return { ...item, text: item.title, type: 1 } }) var categoriesTitles = res[1].filter(item => item.title.includes(value)).map(item => { return { ...item, text: item.title, type: 2 } }) return [...goodsTitles, ...categoriesTitles] }) }, handleSearchResult(e) { console.log(e.detail); var { type, id, title } = e.detail.item if (type === 1) { console.log("详情页面"); wx.navigateTo({ url: `/pages/detail/detail?id=${id}&title=${title}` }) } else { console.log("搜索列表"); wx.navigateTo({ url: `/pages/searchlist/searchlist?id=${id}&title=${title}` }) } } })
|

搜索列表 和 排序
搜索列表和排序:列表布局、详情页跳转、图标引入、价格排序和评价排序
1 2 3 4 5 6 7 8 9
| <view class="goodcontainer"> <view wx:for="{{goodlist}}" wx:key="index" class="good" bindtap="handleGoToDetail" data-id="{{item.id}}" data-title="{{item.title}}"> <image src="{{BASE_URL + item.poster}}" mode="widthFix" /> <view>{{item.title}}</view> <view>价格:<text style="color: red">¥{{item.price}}</text></view> <view>好评率:<text style="color: green">{{item.goodcomment}}</text></view> </view> </view>
|
1 2 3 4 5
| { "usingComponents": { "mp-icon": "weui-miniprogram/icon/icon" } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| .goodcontainer { display: flex; flex-wrap: wrap; } .good { width: 50%; padding: 20rpx; box-sizing: border-box; text-align: center; } .good image{ width: 100%; }
.sort-header { display: flex; flex-direction: row; justify-content: space-around; height: 80rpx; line-height: 80rpx; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| const { request } = require("../../utils/request");
Page({
data: { BASE_URL: '', goodlist: [], priceOrder: true, commentOrder: true },
onLoad(options) { this.setData({ BASE_URL: getApp().globalData.BASE_URL }); console.log(options); wx.setNavigationBarTitle({ title: options.title }) this.getList(options.id) }, getList(id) { request({ url: `/categories?id=${id}&_embed=goods` }).then(res => { console.log(res[0]); this.setData({ goodlist: res[0].goods }) }).catch(err => { console.error(err); }) }, handleGoToDetail(evt) { var { id, title } = evt.currentTarget.dataset wx.navigateTo({ url: `/pages/detail/detail?id=${id}&title=${title}` }) }, handlePriceOrder() { console.log(this.data.priceOrder); this.setData({ priceOrder: !this.data.priceOrder, goodlist: this.data.priceOrder ? this.data.goodlist.sort((x, y) => y.price - x.price) : this.data.goodlist.sort((x, y) => x.price - y.price) }) }, handleCommentOrder() { console.log(this.data.commentOrder); this.setData({ commentOrder: !this.data.commentOrder, goodlist: this.data.commentOrder ? this.data.goodlist.sort((x, y) => parseInt(y.goodcomment) - parseInt(x.goodcomment)) : this.data.goodlist.sort((x, y) => parseInt(x.goodcomment) - parseInt(y.goodcomment)) }) }, ... })
|

7. 分类模块
vtabs 组件
数据渲染
8. 授权模块(★)
微信授权
手机绑定
9. 购物车模块
加入购物车
购物车布局
数据渲染
金额试算
删除
全选
10. 我的模块
布局
换头像