微信小程序06-综合项目点餐系统
零、文章目录
微信小程序06-综合项目点餐系统
1、项目开发准备
(1)开发背景
- 现如今,相比以服务员为中介完成点餐、送餐、买单的传统点餐方式,越来越多的餐厅开始使用微信小程序进行点餐。商家可以在微信小程序中添加点餐和收款功能,顾客可以实现点餐、付款等功能,顾客可以提前进行点餐,商家提前进行备餐,减少等候时间,提升用户体验。
- 微信小程序中有“转发给朋友”“分享到朋友圈”等功能,商家可以通过提供优惠券的方式刺激顾客进行转发、分享。而顾客由于可以享受到优惠,所以很愿意进行转发,当顾客进行转发、分享后,往往会为商家带来大量流量。
(2)项目模块划分
- **用户登录:**当“点餐”微信小程序启动后,会自动进行用户登录。
- **商家首页:**包括轮播图区域、中间区域和底部区域,为用户提供了直观的界面需求。在商家首页点击“开始点餐之旅”,跳转到菜单列表页进行点餐。
- **菜单列表页:**左侧为菜单栏区域,右侧为商品列表区域,点击左侧菜单栏可以定位到右侧相应位置。每个商品列表项包括图片、价格、标题、“+”等信息。用户可以在菜单列表页中根据自己的需求选择商品,点击“+”将所选商品加入购物车。
- **购物车:**当购物车中商品数量为0时,点击购物车图标不会展开购物车;当购物车中商品数量不为0时,点击底部购物车图标,在弹出层中显示已选购的商品,包括商品的图片、价格、名称、数量等信息,此时可以对购物车中已选购的商品进行操作,包括动态添加商品数量、实时计算出商品总价格、清空购物车。选购完商品之后,点击“选好了”按钮,跳转到订单确认页。
- **订单确认页:**在订单确认页中,可核对选择的商品是否正确,并可以根据自己的需求填写备注信息,若信息无误,点击“去支付”,会跳转到订单详情页。
- **订单详情页:**包括取餐号、订单信息等。·订单列表页:在订单列表页可以查看订单状态,是否取餐,若已经取餐会标识“已取餐”,若未取餐会标识“未取餐”。点击“查看详情”可以跳转到订单详情页。
- **消费记录页:**消费记录页显示了历史消费记录信息。
(3)项目初始化
- ①创建项目。在微信开发者工具中创建一个新的微信小程序项目,项目名称为“点餐”,模板选择“不使用模板”。
- ②配置页面。项目创建完成后,在app.json文件中配置页面。
"pages": [
"pages/index/index",
"pages/list/list",
"pages/order/checkout/checkout",
"pages/order/detail/detail",
"pages/order/list/list",
"pages/record/record"
- ③配置导航栏。在app.json文件中配置导航栏样式。
1 "window": {
2 "backgroundTextStyle": "light",
3 "navigationBarBackgroundColor": "#FF9C35",
4 "navigationBarTitleText": "美食屋",
5 "navigationBarTextStyle": "white"
6 },
- ④创建其他文件。
- 创建app.wxss文件,该文件中保存了本项目所用到的公共样式。
- 创建images文件夹,该文件夹保存了该项目所用的素材。
- 创建utils/shopcartAnimate.js文件,该文件保存了实现购物车中动画效果的代码。
- 创建utils/decodeCookie.js文件,该文件保存了用于解析服务器返回的Cookie,将Cookie字符串转换成对象的代码。
- ⑤配置标签栏。在app.json文件中添加tabBar配置项的属性配置标签栏
"tabBar": {
"color": "#8a8a8a",
"selectedColor": "#FF9C35",
"borderStyle": "black",
"list": [{
"selectedIconPath": "images/home_s.png",
"iconPath": "images/home.png",
"pagePath": "pages/index/index",
"text": "首页"
}, {
"selectedIconPath": "images/order_s.png",
"iconPath": "images/order.png",
"pagePath": "pages/order/list/list",
"text": "订单"
}, {
"selectedIconPath": "images/user_s.png",
"iconPath": "images/user.png",
"pagePath": "pages/record/record",
"text": "我的"
}]
},
- 目录结构如下
2、封装网络请求
(1)保存接口地址
- 在实际项目开发中,很多页面的请求地址URL的前半部分都是相同的,重复书写会导致代码冗余,而且如果请求地址更换了域名,修改也比较麻烦。在本项目中,会将URL的公共部分提取出来,单独放置到配置文件中,从而方便后期修改。
- 在utils文件夹下新建config.js文件,在utils/config.js文件中编写URL的公共部分,具体代码如下。
module.exports = {
baseUrl: 'http://localhost/api'
}
(2)封装网络请求函数
- 由于wx.request()方法是一个异步方法,利用Promise可以简化异步操作。在编写服务器接口地址时,可以自动拼接URL的公共部分,使用时只需要传入请求参数即可。
- 在utils文件夹下新建fetch.js文件,在fetch.js文件中编写封装网络请求的代码,具体如下。
const config = require('./config.js')
const decodeCookie = require('./decodeCookie.js')
var sess = wx.getStorageSync('PHPSESSID')
module.exports = function (path, data, method) {
return new Promise((resolve, reject) => {
wx.request({
url: config.baseUrl + path,
method: method,
data: data,
header: {
'Cookie': sess ? 'PHPSESSID=' + sess : ''
},
success: res => {
if (res.header['Set-Cookie'] !== undefined) {
sess = decodeCookie(res.header['Set-Cookie'])['PHPSESSID']
wx.setStorageSync('PHPSESSID', sess)
}
// 请求成功
if (res.statusCode !== 200) {
fail('服务器异常', reject)
return
}
if (res.data.code === 0) {
fail(res.data.msg, reject)
return
}
resolve(res.data)
},
fail: function () {
// 请求失败
fail('加载数据失败', reject)
}
})
})
}
function fail(title, callback) {
wx.hideLoading()
wx.showModal({
title: title,
confirmText: '重试',
success: res => {
if (res.confirm) {
callback()
}
}
})
}
- 在app.js文件中引入fetch.js文件,方便在整个项目使用,具体代码如下。
App({
fetch: require('./utils/fetch.js'),
})
- 在pages/index/index.js文件中发送网络请求,具体代码如下。
// index.js
const app = getApp()
const fetch = app.fetch
Page({
data: {
swiper: [],
ad: '',
category: []
},
onLoad: function () {
var callback = () => {
wx.showLoading({
title: '努力加载中',
mask: true
})
fetch('/food/index').then(data => {
wx.hideLoading()
this.setData({
swiper: data.img_swiper,
ad: data.img_ad,
category: data.img_category
})
}, () => {
callback()
})
}
if (app.userLoginReady) {
callback()
} else {
app.userLoginReadyCallback = callback
}
},
start: function () {
wx.navigateTo({
url: '/pages/list/list',
})
}
})
3、用户登录
(1)判断登录状态
- 在微信小程序启动时,需要通过/user/checkLogin接口判断是否处于登录状态。
- 在app.js文件中编写onLaunch()函数,实现判断登录状态。
(2)执行登录操作
- 当用户未登录时,需要调用wx.login()方法执行登录操作。
- wx.login()方法执行成功后会通过success回调函数的参数返回code,即用户登录凭证。
- 然后发起网络请求将code发送给服务器接口’/user/login’进行校验,从而让服务器识别用户身份。
(3)记住登录状态
-
在用户登录成功后,服务器为了记住用户的状态,会返回一个自定义登录态。在本项目中,自定义登录态是通过会话技术实现的,服务器会响应一个名称为PHPSESSID的Cookie给客户端,客户端需要记住服务器返回的Cookie,并在下次请求中发送Cookie,这样可以让服务器能够辨别用户身份。在后续发送请求的时候需要携带Cookie。在Cookie有效期内,如果微信小程序重新启动了,仍然维持已登录的状态。
-
在本项目中,微信小程序应先读取本地缓存中的Cookie,如果读取结果为空字符串说明用户未登录。当用户登录成功后,需要从服务器返回的Set-Cookie响应头中取出名称为PHPSESSID的Cookie,将它保存到本地缓存中,然后在wx.request()方法发起请求时,将Cookie附加到请求头中传递
(4)app.js完整代码
// app.js
App({
fetch: require('./utils/fetch.js'),
onLaunch: function () {
wx.showLoading({
title: '登录中',
mask: true
})
this.fetch('/user/checkLogin').then(data => {
if (data.isLogin) {
// 已登录
this.onUserLoginReady()
console.log('通过保存的Cookie登录成功') // 新增代码
} else {
// 未登录
this.login({
success: () => { // 登录成功
this.onUserLoginReady()
},
fail: () => { // 登录失败,重新登录
this.onLaunch()
}
})
}
}, () => {
this.onLaunch()
})
},
login: function (options) {
wx.login({
success: res => {
this.fetch('/user/login', {
js_code: res.code
}).then(data => {
if (data && data.isLogin) {
options.success()
} else {
wx.hideLoading()
wx.showModal({
title: '登录失败(请使用真实的AppID,并检查服务器端配置)',
confirmText: '重试',
success: res => {
if (res.confirm) {
options.fail()
}
}
})
}
}, () => {
options.fail()
})
}
})
},
userLoginReady: false,
userLoginReadyCallback: null,
onUserLoginReady: function() {
wx.hideLoading()
if (this.userLoginReadyCallback) {
this.userLoginReadyCallback()
}
this.userLoginReady = true
}
})
4、商家首页
(1)实现页面结构
- 在pages/index/index.wxml文件中实现商家首页页面结构。
<!--index.wxml-->
<swiper class="swiper" indicator-dots="true" autoplay="true" interval="5000" duration="1000">
<block wx:for="{{ swiper }}" wx:key="*this">
<swiper-item>
<image src="{{ item }}" />
</swiper-item>
</block>
</swiper>
<!-- 开启点餐之旅 -->
<view class="menu-bar">
<view class="menu-block" bindtap="start">
<view class="menu-start">开启点餐之旅→</view>
</view>
</view>
<!-- 最新消息展示 -->
<view class="ad-box">
<image src="{{ ad }}" class="ad-image" />
</view>
<view class="bottom-box">
<view class="bottom-pic" wx:for="{{ category }}" wx:key="index">
<image src="{{ item }}" class="bottom-image" />
</view>
</view>
(2)实现页面样式
- 在pages/index/index.wxss文件中编写页面样式。
/**index.wxss**/
.swiper {
height: 350rpx;
}
.swiper image {
width: 100%;
height: 100%;
}
.menu-bar {
display: flex;
margin-top: 20rpx;
}
.menu-block {
display: flex;
justify-content: center;
margin: 0 auto;
}
.menu-start {
text-align: center;
font-size: 38rpx;
color: #fff;
padding: 16rpx 80rpx;
background: #ff9c35;
border-radius: 80rpx;
}
.ad-box {
margin-top: 20rpx;
width: 100%;
text-align: center;
}
.ad-image {
width: 710rpx;
height: 336rpx;
}
.bottom-box {
margin: 20rpx 0;
width: 100%;
box-sizing: border-box;
padding: 0 20rpx;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
}
.bottom-pic {
width: 49%;
display: inline-block;
}
.bottom-image {
width: 100%;
height: 170rpx;
}
(3)实现页面逻辑
- 在pages/index/index.js文件中实现页面逻辑。
// index.js
const app = getApp()
const fetch = app.fetch
Page({
data: {
swiper: [],
ad: '',
category: []
},
onLoad: function () {
var callback = () => {
wx.showLoading({
title: '努力加载中',
mask: true
})
fetch('/food/index').then(data => {
wx.hideLoading()
this.setData({
swiper: data.img_swiper,
ad: data.img_ad,
category: data.img_category
})
}, () => {
callback()
})
}
if (app.userLoginReady) {
callback()
} else {
app.userLoginReadyCallback = callback
}
},
start: function () {
wx.navigateTo({
url: '/pages/list/list',
})
}
})
(4)实现页面效果
5、菜单列表页
(1)实现页面结构
- 在pages/list/list.wxml文件中实现页面结构
<!--pages/list/list.wxml-->
<view class="discount">
<text class="discount-txt">减</text>满{{ promotion.k }}元减{{ promotion.v }}元(在线支付专享)
</view>
<view class="content">
<!-- 左侧菜单栏区域 -->
<scroll-view class="category" scroll-y>
<view wx:for="{{ foodList }}" wx:key="id" class="category-item category-{{ activeIndex == index ? 'selected' : 'unselect' }}" data-index="{{ index }}" bindtap="tapCategory">
<view class="category-name">{{ item.name }}</view>
</view>
</scroll-view>
<!-- 右侧商品列表区域 -->
<scroll-view class="food" scroll-y scroll-into-view="category_{{ tapIndex }}" scroll-with-animation bindscroll="onFoodScroll">
<block wx:for="{{ foodList }}" wx:for-item="category" wx:key="id" wx:for-index="category_index">
<view class="food-category" id="category_{{ category_index }}">{{ category.name }}</view>
<view class="food-item" wx:for="{{ category.food }}" wx:for-item="food" wx:key="id">
<view class="food-item-pic">
<image mode="widthFix" src="{{ food.image_url }}" />
</view>
<view class="food-item-info">
<view>{{ food.name }}</view>
<view class="food-item-price">{{ priceFormat(food.price) }}</view>
</view>
<view class="food-item-opt">
<i class="iconfont" data-category_index="{{ category_index }}" data-index="{{ index }}" bindtap="addToCart"></i>
</view>
</view>
</block>
</scroll-view>
</view>
<!-- 购物车界面 -->
<view class="shopcart" wx:if="{{ showCart }}">
<view class="shopcart-mask" bindtap="showCartList" wx:if="{{ showCart }}"></view>
<view class="shopcart-wrap">
<view class="shopcart-head">
<view class="shopcart-head-title">已选商品</view>
<view class="shopcart-head-clean" bindtap="cartClear">
<i class="iconfont"></i>清空购物车
</view>
</view>
<view class="shopcart-list">
<view class="shopcart-item" wx:for="{{ cartList }}" wx:key="id">
<view class="shopcart-item-name">{{ item.name }}</view>
<view class="shopcart-item-price">
<view>{{ priceFormat(item.price * item.number) }}</view>
</view>
<view class="shopcart-item-number">
<i class="iconfont shopcart-icon-dec" data-id="{{ index }}" bindtap="cartNumberDec"></i>
<view>{{ item.number }}</view>
<i class="iconfont shopcart-icon-add" data-id="{{ index }}" bindtap="cartNumberAdd"></i>
</view>
</view>
</view>
</view>
</view>
<!-- 满减优惠信息 -->
<view class="promotion">
<label wx:if="{{ promotion.k - cartPrice > 0 }}">满{{ promotion.k }}立减{{ promotion.v }}元,还差{{ promotion.k - cartPrice }}元</label>
<label wx:else>已满{{ promotion.k }}元可减{{ promotion.v }}元</label>
</view>
<!-- 小球动画 -->
<view class="operate">
<view class="operate-shopcart-ball" hidden="{{ !cartBall.show }}" style="left: {{ cartBall.x }}px; top: {{ cartBall.y }}px;"></view>
<view class="operate-shopcart" bindtap="showCartList">
<i class="iconfont operate-shopcart-icon {{ cartNumber > 0 ? 'operate-shopcart-icon-activity' : '' }}">
<span wx:if="{{ cartNumber > 0 }}">{{ cartNumber }}</span>
</i>
<view class="operate-shopcart-empty" wx:if="{{ cartNumber === 0 }}">购物车是空的</view>
<view class="operate-shopcart-price" wx:else>
<block wx:if="{{ cartPrice >= promotion.k }}">
<view>{{ priceFormat(cartPrice - promotion.v )}}</view>
<text>{{ priceFormat(cartPrice) }}</text>
</block>
<view wx:else>{{ priceFormat(cartPrice) }}</view>
</view>
</view>
<view class="operate-submit {{ cartNumber !== 0 ? 'operate-submit-activity' : '' }}" bindtap="order">选好了</view>
</view>
<wxs module="priceFormat">
module.exports = function (price) {
return '¥ ' + parseFloat(price)
}
</wxs>
(2)实现页面样式
- 在pages/list/list.wxss文件中实现页面样式
/* pages/list/list.wxss */
page {
display: flex;
flex-direction: column;
height: 100%;
}
/* 折扣信息区 */
.discount {
width: 100%;
height: 70rpx;
line-height: 70rpx;
background: #fef9e6;
font-size: 28rpx;
text-align: center;
color: #999;
}
.discount-txt {
color: #fff;
padding: 5rpx 10rpx;
background: red;
margin-right: 15rpx;
}
.content {
flex: 1;
display: flex;
overflow: hidden;
}
.category {
width: 202rpx;
height: 100%;
background: #fcfcfc;
font-size: 28rpx;
}
/* 隐藏滚动条 */
::-webkit-scrollbar {
width: 0;
height: 0;
color: transparent;
}
.category-item {
height: 100rpx;
line-height: 100rpx;
text-align: center;
}
.food-category {
font-size: 24rpx;
background: #f3f4f6;
padding: 10rpx;
color: #ff9c35;
}
.food-item {
display: flex;
margin: 40rpx 20rpx;
}
.food-item-pic {
margin-right: 20rpx;
width: 94rpx;
height: 94rpx;
}
.food-item-pic > image {
width: 100%;
height: 100%;
}
.food-item-info {
flex: 1;
font-size: 30rpx;
margin-top: 4rpx;
}
.food-item-price {
margin-top: 14rpx;
color: #f05a86;
}
.food-item-opt {
margin-top: 40rpx;
}
.food-item-opt > i:before {
font-size: 44rpx;
color: #ff9c35;
content: "\e728";
}
.category-unselect {
color: #6c6c6c;
background: #f9f9f9;
border-bottom: 1rpx solid #e3e3e3;
}
.category-selected {
color: #ff9c35;
background: white;
border-left: 6rpx solid #ff9c35;
}
.category-selected:last-child {
border-bottom: 1rpx solid #e3e3e3;
}
/* 购物车区域 */
.operate {
height: 110rpx;
display: flex;
}
.operate-shopcart {
display: flex;
width: 74%;
padding: 10rpx;
background: #353535;
}
/* “选好了”按钮 */
.operate-submit {
width: 26%;
font-size: 30rpx;
background: #eee;
color: #aaa;
text-align: center;
line-height: 110rpx;
}
.operate-submit-activity {
background: #ff9c35;
color: #fff;
}
/* 购物车图标 */
.operate-shopcart-icon {
font-size: 80rpx;
color: #87888e;
margin-left: 20rpx;
position: relative;
}
.operate-shopcart-icon:before {
content: "\e73c";
}
.operate-shopcart-icon-activity {
color: #ff9c35;
}
/* 购物车为空 */
.operate-shopcart-empty {
color: #a9a9a9;
line-height: 88rpx;
font-size: 30rpx;
margin-left: 20rpx;
}
/* 购物车中的商品购买数量 */
.operate-shopcart-icon > span {
padding: 2rpx 14rpx;
border-radius: 50%;
background: red;
color: white;
font-size: 28rpx;
position: absolute;
top: 0px;
right: -10rpx;
text-align: center;
}
/* 购物车中的商品价格 */
.operate-shopcart-price {
display: flex;
}
.operate-shopcart-price > view {
font-size: 40rpx;
line-height: 88rpx;
margin-left: 25rpx;
color: #fff;
}
.operate-shopcart-price > text {
font-size: 24rpx;
line-height: 92rpx;
margin-left: 15rpx;
color: #aaa;
text-decoration: line-through;
}
/* 小球的样式 */
.operate-shopcart-ball {
width: 36rpx;
height: 36rpx;
position: fixed;
border-radius: 50%;
left: 50%;
top: 50%;
background: #ff9c35;
}
/* 满减优惠区域 */
.promotion {
padding: 7rpx 0 9rpx;
background: #ffcd9b;
color: #fff7ec;
font-size: 28rpx;
text-align: center;
}
.shopcart {
position: fixed;
top: 0;
left: 0;
bottom: 149rpx;
right: 0;
font-size: 28rpx;
}
.shopcart-wrap {
position: absolute;
width: 100%;
max-height: 90%;
bottom: 0;
background: #fff;
overflow: scroll;
}
.shopcart-mask {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: #000;
opacity: 0.5;
}
.shopcart-head {
position: fixed;
width: 100%;
background: #f0f0f0;
color: #878787;
line-height: 100rpx;
font-size: 26rpx;
overflow: hidden;
}
.shopcart-head-title {
float: left;
margin-left: 40rpx;
}
.shopcart-head-title:before {
background: #ff9c35;
width: 8rpx;
height: 32rpx;
content: "";
display: inline-block;
margin-right: 10rpx;
position: relative;
top: 6rpx;
}
.shopcart-head-clean {
float: right;
margin-right: 20rpx;
}
.shopcart-head-clean > i:before {
content: "\e61b";
position: relative;
top: 2rpx;
}
.shopcart-list {
margin-top: 101rpx;
}
.shopcart-item {
display: flex;
padding: 30rpx 20rpx;
line-height: 40rpx;
}
.shopcart-item > view {
margin-left: 30rpx;
}
.shopcart-item:not(:last-child) {
border-bottom: 1rpx solid #e3e3e3;
}
.shopcart-item-name {
flex: 1;
}
.shopcart-item-price {
color: #ff9c35;
}
.shopcart-item-number {
display: flex;
}
.shopcart-item-number > view {
margin: 0 15rpx;
}
.shopcart-icon-dec:before {
content: "\e61a";
font-size: 44rpx;
color: #888;
}
.shopcart-icon-add:before {
content: "\e728";
font-size: 44rpx;
color: #ff9c35;
}
(3)实现页面逻辑
- 在utils/shopcartAnimate.js中实现购物车动画
module.exports = function (iconClass, page) {
var busPos = {}
wx.createSelectorQuery().select(iconClass).boundingClientRect(rect => {
busPos.x = rect.left + 15
busPos.y = rect.top
}).exec()
return {
start: function (e) {
var finger = {
x: e.touches[0].clientX - 10,
y: e.touches[0].clientY - 10
}
var topPoint = {}
if (finger.y < busPos.y) {
topPoint.y = finger.y - 150
} else {
topPoint.y = busPos.y - 150
}
topPoint.x = Math.abs(finger.x - busPos.x) / 2
if (finger.x > busPos.x) {
topPoint.x = (finger.x - busPos.x) / 2 + busPos.x
} else {
topPoint.x = (busPos.x - finger.x) / 2 + finger.x
}
var linePos = bezier([busPos, topPoint, finger], 30)
var bezier_points = linePos.bezier_points
page.setData({
'cartBall.show': true,
'cartBall.x': finger.x,
'cartBall.y': finger.y
})
var len = bezier_points.length
var index = len
let i = index - 1
var timer = setInterval(function () {
i = i - 5
if (i < 1) {
clearInterval(timer)
page.setData({
'cartBall.show': false
})
return
}
page.setData({
'cartBall.show': true,
'cartBall.x': bezier_points[i].x,
'cartBall.y': bezier_points[i].y
})
}, 50)
}
}
function bezier(pots, amount) {
var pot
var lines
var ret = []
var points
for (var i = 0; i <= amount; ++i) {
points = pots.slice(0)
lines = []
while (pot = points.shift()) {
if (points.length) {
lines.push(pointLine([pot, points[0]], i / amount))
} else if (lines.length > 1) {
points = lines
lines = []
} else {
break
}
}
ret.push(lines[0])
}
function pointLine(points, rate) {
var pointA, pointB, pointDistance, xDistance, yDistance, tan, radian, tmpPointDistance
var ret = []
pointA = points[0]
pointB = points[1]
xDistance = pointB.x - pointA.x
yDistance = pointB.y - pointA.y
pointDistance = Math.pow(Math.pow(xDistance, 2) + Math.pow(yDistance, 2), 1 / 2)
tan = yDistance / xDistance
radian = Math.atan(tan)
tmpPointDistance = pointDistance * rate
ret = {
x: pointA.x + tmpPointDistance * Math.cos(radian),
y: pointA.y + tmpPointDistance * Math.sin(radian)
}
return ret
}
return {
bezier_points: ret
}
}
}
- 在pages/list/list.js文件中实现页面逻辑
// pages/list/list.js
// 引入购物车动画模块
const shopcartAnimate = require('../../utils/shopcartAnimate.js')
const app = getApp()
const fetch = app.fetch
const categoryPosition = [] // 右列表各分类高度数组
Page({
data: {
foodList: [],
promotion: {},
activeIndex: 0,
tapIndex: 0,
cartPrice: 0, // 购物车中商品的总价格
cartNumber: 0, // 购物车中商品的总数量
cartList: {}, // 保存购物车数据
showCart: false,
},
disableNextScroll: false,
shopcartAnimate: null,
onLoad: function () {
wx.showLoading({
title: '努力加载中'
})
fetch('/food/list').then(data => {
wx.hideLoading()
this.setData({
foodList: data.list,
promotion: data.promotion[0]
}, () => {
var query = wx.createSelectorQuery()
var top = 0
var height = 0
query.select('.food').boundingClientRect(rect => {
top = rect.top
height = rect.height
})
query.selectAll('.food-category').boundingClientRect(res => {
res.forEach(rect => {
categoryPosition.push(rect.top - top - height / 3)
})
})
query.exec()
})
}, () => {
this.onLoad()
})
this.shopcartAnimate = shopcartAnimate('.operate-shopcart-icon', this)
},
tapCategory: function (e) {
this.disableNextScroll = true
var index = e.currentTarget.dataset.index
this.setData({
activeIndex: index,
tapIndex: index
})
},
onFoodScroll: function (e) {
if (this.disableNextScroll) {
this.disableNextScroll = false
return
}
var scrollTop = e.detail.scrollTop
var activeIndex = 0
categoryPosition.forEach((item, i) => {
if (scrollTop >= item) {
activeIndex = i
}
})
if (activeIndex !== this.data.activeIndex) {
this.setData({ activeIndex })
}
},
// 加入购物车
addToCart: function (e) {
const index = e.currentTarget.dataset.index
const category_index = e.currentTarget.dataset.category_index
const food = this.data.foodList[category_index].food[index]
const cartList = this.data.cartList
if (cartList[index]) {
++cartList[index].number
} else {
cartList[index] = {
id: food.id,
name: food.name,
price: parseFloat(food.price),
number: 1
}
}
this.setData({
cartList,
cartPrice: this.data.cartPrice + cartList[index].price,
cartNumber: this.data.cartNumber + 1
})
this.shopcartAnimate.start(e)
},
showCartList: function () {
if (this.data.cartNumber > 0) {
this.setData({
showCart: !this.data.showCart
})
}
},
cartNumberAdd: function(e) {
var id = e.currentTarget.dataset.id
var cartList = this.data.cartList
++cartList[id].number
this.setData({
cartList: cartList,
cartNumber: ++this.data.cartNumber,
cartPrice: this.data.cartPrice + cartList[id].price
})
},
cartNumberDec: function(e) {
var id = e.currentTarget.dataset.id
var cartList = this.data.cartList
if (cartList[id]) {
var price = cartList[id].price
if (cartList[id].number > 1) {
--cartList[id].number
} else {
delete cartList[id]
}
this.setData({
cartList: cartList,
cartNumber: --this.data.cartNumber,
cartPrice: this.data.cartPrice - price
})
if (this.data.cartNumber <= 0) {
this.setData({
showCart: false
})
}
}
},
// 清空购物车
cartClear: function() {
this.setData({
cartList: {},
cartNumber: 0,
cartPrice: 0,
showCart: false
})
},
// 实现跳转到订单确认页
order: function() {
if (this.data.cartNumber === 0) {
return
}
wx.showLoading({
title: '正在生成订单'
})
fetch('/food/order', {
order: this.data.cartList
}, 'POST').then(data => {
wx.navigateTo({
url: '/pages/order/checkout/checkout?order_id=' + data.order_id
})
}, () => {
this.order()
})
}
})
(4)实现页面效果
6、订单确认页
(1)实现页面结构
- 在pages/order/checkout/checkout.wxml文件中实现页面结构
<!--pages/order/checkout/checkout.wxml-->
<view class="content">
<!-- 标题 -->
<view class="content-title">请确认您的订单</view>
<!-- 订单信息-->
<view class="order">
<view class="order-title">订单详情</view>
<view class="order-list">
<!-- 订单商品列表项 -->
<view class="order-item" wx:for="{{ order_food }}" wx:key="id">
<view class="order-item-left">
<image class="order-item-image" mode="widthFix" src="{{ item.image_url }}" />
<view>
<view class="order-item-name">{{ item.name }}</view>
<view class="order-item-number">x {{ item.number }}</view>
</view>
</view>
<view class="order-item-price">{{ priceFormat(item.price * item.number) }}</view>
</view>
<!-- 满减信息 -->
<view class="order-item" wx:if="{{ checkPromotion(promotion) }}">
<view class="order-item-left">
<i class="order-promotion-icon">减</i>满减优惠
</view>
<view class="order-promotion-price">- {{ priceFormat(promotion) }}</view>
</view>
<!-- 小计 -->
<view class="order-item">
<view class="order-item-left">小计</view>
<view class="order-total-price">{{ priceFormat(price) }}</view>
</view>
</view>
</view>
<!-- 备注功能 -->
<view class="content-comment">
<label>备注</label>
<textarea placeholder="如有其他要求,请输入备注" bindinput="inputComment"></textarea>
</view>
</view>
<!-- 支付功能 -->
<view class="operate">
<view class="operate-info">合计:{{ priceFormat(price) }}</view>
<view class="operate-submit" bindtap="pay">去支付</view>
</view>
<!-- 处理商品价格格式 -->
<wxs module="priceFormat">
module.exports = function (price) {
return price ? '¥ ' + parseFloat(price) : ''
}
</wxs>
<wxs module="checkPromotion">
module.exports = function (promotion) {
return parseFloat(promotion) > 0
}
</wxs>
(2)实现页面样式
- 在pages/order/checkout/checkout.wxss文件中实现页面样式
page {
display: flex;
flex-direction: column;
height: 100%;
background: #f8f8f8;
}
.content {
flex: 1;
overflow: scroll;
margin-bottom: 40rpx;
}
::-webkit-scrollbar {
display: none;
}
.content-title {
height: 80rpx;
line-height: 80rpx;
font-size: 28rpx;
background: white;
padding: 0 10rpx;
}
.order {
background: white;
margin-top: 20rpx;
}
.order-title {
font-size: 24rpx;
color: #a2a1a0;
padding: 24rpx;
}
.order-list {
padding: 0 30rpx;
}
.order-item {
background: #fff;
display: flex;
font-size: 32rpx;
padding: 25rpx 0;
border-top: 1rpx #e3e3e3 solid;
}
.order-item-left {
flex: 1;
display: flex;
}
.order-item-image {
width: 94rpx;
height: 94rpx;
margin-right: 25rpx;
}
.order-item-number {
color: #a3a3a3;
margin-top: 4rpx;
font-size: 28rpx;
}
.order-promotion-icon {
display: inline-block;
background: #ff4500;
padding: 2rpx 6rpx 6rpx;
color: #fff;
font-size: 28rpx;
margin-right: 8rpx;
}
.order-promotion-price {
color: #ff4500;
}
.order-total-price {
font-size: 40rpx;
}
.content-comment {
padding: 10rpx 30rpx 20rpx;
background: white;
margin-top: 20rpx;
}
.content-comment > label {
font-size: 32rpx;
color: #a3a3a3;
}
.content-comment > textarea {
width: 95%;
font-size: 24rpx;
background: #f2f2f2;
padding: 20rpx;
height: 160rpx;
margin-top: 10rpx;
}
.operate {
height: 110rpx;
display: flex;
}
.operate-info {
width: 74%;
background: #353535;
color: #fff;
line-height: 110rpx;
padding-left: 40rpx;
}
.operate-submit {
width: 26%;
font-size: 30rpx;
text-align: center;
line-height: 110rpx;
background: #ff9c35;
color: #fff;
}
(3)实现页面逻辑
- 在pages/order/checkout/checkout.js文件中实现页面逻辑
const app = getApp()
const fetch = app.fetch
Page({
data: {},
comment: '',
onLoad: function (options) {
wx.showLoading({
title: '努力加载中'
})
fetch('/food/order', {
id: options.order_id
}).then(data => {
this.setData(data)
wx.hideLoading()
}, () => {
this.onLoad(options)
})
},
inputComment: function (e) {
console.log(e)
this.comment = e.detail.value
},
pay: function () {
var id = this.data.id
wx.showLoading({
title: '正在支付'
})
fetch('/food/order', {
id: id,
comment: this.comment
}, 'POST').then(() => {
return fetch('/food/pay', { id }, 'POST')
}).then(() => {
wx.hideLoading()
wx.showToast({
title: '支付成功',
icon: 'success',
duration: 2000,
success: () => {
wx.navigateTo({
url: '/pages/order/detail/detail?order_id=' + id
})
}
})
}).catch(() => {
this.pay()
})
}
})
(4)实现页面效果
7、订单详情页
(1)实现页面结构
- 在pages/order/detail/detail.wxml文件中实现页面结构
<!--pages/order/detail/detail.wxml-->
<view class="top">
<view class="card" wx:if="{{ !is_taken }}">
<view class="card-title">取餐号</view>
<view class="card-content">
<view class="card-info">
<text class="card-code">{{ code }}</text>
<text class="card-info-r">正在精心制作中…</text>
</view>
<view class="card-comment" wx:if="{{ comment }}">备注:{{ comment }}</view>
<view class="card-tips">美食制作中,尽快为您服务☺</view>
</view>
</view>
</view>
<view class="order">
<view class="order-title">订单详情</view>
<view class="order-list">
<!-- 订单商品列表项 -->
<view class="order-item" wx:for="{{ order_food }}" wx:key="id">
<view class="order-item-left">
<image class="order-item-image" mode="widthFix" src="{{ item.image_url }}" />
<view>
<view class="order-item-name">{{ item.name }}</view>
<view class="order-item-number">x {{ item.number }}</view>
</view>
</view>
<view class="order-item-price">{{ priceFormat(item.price * item.number) }}</view>
</view>
<!-- 满减信息 -->
<view class="order-item" wx:if="{{ checkPromotion(promotion) }}">
<view class="order-item-left">
<i class="order-promotion-icon">减</i>满减优惠
</view>
<view class="order-promotion-price">- {{ priceFormat(promotion) }}</view>
</view>
<!-- 小计 -->
<view class="order-item">
<view class="order-item-left">小计</view>
<view class="order-total-price">{{priceFormat(price)}}</view>
</view>
</view>
</view>
<view class="list">
<view>
<text>订单号码</text>
<view>{{ sn }}</view>
</view>
<view>
<text>下单时间</text>
<view>{{ create_time }}</view>
</view>
<view>
<text>付款时间</text>
<view>{{ pay_time }}</view>
</view>
<view wx:if="{{ is_taken }}">
<text>取餐时间</text>
<view>{{ taken_time }}</view>
</view>
</view>
<view class="tips" wx:if="{{ is_taken }}">取餐号{{ code }} 您已取餐</view>
<view class="tips" wx:else>请凭此页面至取餐柜台领取美食</view>
<wxs module="priceFormat">
module.exports = function (price) {
return price ? '¥ ' + parseFloat(price) : ''
}
</wxs>
<wxs module="checkPromotion">
module.exports = function (promotion) {
return parseFloat(promotion) > 0
}
</wxs>
(2)实现页面样式
- 在pages/order/detail/detail.wxss文件中实现页面样式
/* pages/order/detail/detail.wxss */
.card {
margin: 20rpx auto;
width: 85%;
background: #fef9f4;
display: flex;
font-size: 30rpx;
}
.card-title {
width: 28rpx;
padding: 0 30rpx;
background: #de5f4b;
border-left: 1rpx solid #de5f4b;
font-size: 28rpx;
color: #fff;
display: flex;
align-items: center;
}
.card-content {
flex: 1;
margin-left: 50rpx;
}
.card-info {
margin-top: 10rpx;
}
.card-code {
font-size: 60rpx;
margin-right: 40rpx;
}
.card-info-r {
font-size: 24rpx;
color: #ff9c35;
}
.card-comment {
color: #de5f4b;
font-weight: 600;
margin-top: 8rpx;
}
.card-tips {
color: #a2a1a0;
margin: 10rpx 0 20rpx;
font-size: 24rpx;
}
.order {
background: white;
margin-top: 20rpx;
}
.order-title {
font-size: 24rpx;
color: #a2a1a0;
padding: 24rpx;
}
.order-list {
padding: 0 30rpx;
}
.order-item {
background: #fff;
display: flex;
font-size: 32rpx;
padding: 25rpx 0;
border-top: 1rpx #e3e3e3 solid;
}
.order-item-left {
flex: 1;
display: flex;
}
.order-item-image {
width: 94rpx;
height: 94rpx;
margin-right: 25rpx;
}
.order-item-number {
color: #a3a3a3;
margin-top: 4rpx;
font-size: 28rpx;
}
.order-promotion-icon {
display: inline-block;
background: #ff4500;
padding: 2rpx 6rpx 6rpx;
color: #fff;
font-size: 28rpx;
margin-right: 8rpx;
}
.order-promotion-price {
color: #ff4500;
}
.order-total-price {
font-size: 40rpx;
}
.list {
background: #fff;
margin-top: 20rpx;
}
.list > view {
font-size: 30rpx;
color: #d1d1d1;
padding: 20rpx;
border-bottom: 1rpx #e3e3e3 solid;
display: flex;
}
.list > view > view {
color: black;
margin-left: 20rpx;
}
.tips {
width: 80%;
text-align: center;
margin: 20rpx auto 40rpx;
padding: 12rpx 20rpx;
background: #ff9c35;
color: #fff;
font-size: 36rpx;
}
(3)实现页面逻辑
- 在pages/order/detail/detail.js文件中实现页面逻辑
// pages/order/detail/detail.js
const app = getApp()
const fetch = app.fetch
Page({
data: {},
onLoad: function (options) {
var id = options.order_id
wx.showLoading({
title: '努力加载中'
})
fetch('/food/order', {
id: id
}).then(data => {
this.setData(data)
wx.hideLoading()
}, () => {
this.onLoad(options)
})
},
onUnload: function () {
wx.reLaunch({
url: '/pages/order/list/list'
})
}
})
(4)实现页面效果
8、订单列表页
(1)实现页面结构
- 在pages/order/list/list.wxml文件中实现页面结构
<!--pages/list/list.wxml-->
<view class="discount">
<text class="discount-txt">减</text>满{{ promotion.k }}元减{{ promotion.v }}元(在线支付专享)
</view>
<view class="content">
<!-- 左侧菜单栏区域 -->
<scroll-view class="category" scroll-y>
<view wx:for="{{ foodList }}" wx:key="id" class="category-item category-{{ activeIndex == index ? 'selected' : 'unselect' }}" data-index="{{ index }}" bindtap="tapCategory">
<view class="category-name">{{ item.name }}</view>
</view>
</scroll-view>
<!-- 右侧商品列表区域 -->
<scroll-view class="food" scroll-y scroll-into-view="category_{{ tapIndex }}" scroll-with-animation bindscroll="onFoodScroll">
<block wx:for="{{ foodList }}" wx:for-item="category" wx:key="id" wx:for-index="category_index">
<view class="food-category" id="category_{{ category_index }}">{{ category.name }}</view>
<view class="food-item" wx:for="{{ category.food }}" wx:for-item="food" wx:key="id">
<view class="food-item-pic">
<image mode="widthFix" src="{{ food.image_url }}" />
</view>
<view class="food-item-info">
<view>{{ food.name }}</view>
<view class="food-item-price">{{ priceFormat(food.price) }}</view>
</view>
<view class="food-item-opt">
<i class="iconfont" data-category_index="{{ category_index }}" data-index="{{ index }}" bindtap="addToCart"></i>
</view>
</view>
</block>
</scroll-view>
</view>
<!-- 购物车界面 -->
<view class="shopcart" wx:if="{{ showCart }}">
<view class="shopcart-mask" bindtap="showCartList" wx:if="{{ showCart }}"></view>
<view class="shopcart-wrap">
<view class="shopcart-head">
<view class="shopcart-head-title">已选商品</view>
<view class="shopcart-head-clean" bindtap="cartClear">
<i class="iconfont"></i>清空购物车
</view>
</view>
<view class="shopcart-list">
<view class="shopcart-item" wx:for="{{ cartList }}" wx:key="id">
<view class="shopcart-item-name">{{ item.name }}</view>
<view class="shopcart-item-price">
<view>{{ priceFormat(item.price * item.number) }}</view>
</view>
<view class="shopcart-item-number">
<i class="iconfont shopcart-icon-dec" data-id="{{ index }}" bindtap="cartNumberDec"></i>
<view>{{ item.number }}</view>
<i class="iconfont shopcart-icon-add" data-id="{{ index }}" bindtap="cartNumberAdd"></i>
</view>
</view>
</view>
</view>
</view>
<!-- 满减优惠信息 -->
<view class="promotion">
<label wx:if="{{ promotion.k - cartPrice > 0 }}">满{{ promotion.k }}立减{{ promotion.v }}元,还差{{ promotion.k - cartPrice }}元</label>
<label wx:else>已满{{ promotion.k }}元可减{{ promotion.v }}元</label>
</view>
<!-- 小球动画 -->
<view class="operate">
<view class="operate-shopcart-ball" hidden="{{ !cartBall.show }}" style="left: {{ cartBall.x }}px; top: {{ cartBall.y }}px;"></view>
<view class="operate-shopcart" bindtap="showCartList">
<i class="iconfont operate-shopcart-icon {{ cartNumber > 0 ? 'operate-shopcart-icon-activity' : '' }}">
<span wx:if="{{ cartNumber > 0 }}">{{ cartNumber }}</span>
</i>
<view class="operate-shopcart-empty" wx:if="{{ cartNumber === 0 }}">购物车是空的</view>
<view class="operate-shopcart-price" wx:else>
<block wx:if="{{ cartPrice >= promotion.k }}">
<view>{{ priceFormat(cartPrice - promotion.v )}}</view>
<text>{{ priceFormat(cartPrice) }}</text>
</block>
<view wx:else>{{ priceFormat(cartPrice) }}</view>
</view>
</view>
<view class="operate-submit {{ cartNumber !== 0 ? 'operate-submit-activity' : '' }}" bindtap="order">选好了</view>
</view>
<wxs module="priceFormat">
module.exports = function (price) {
return '¥ ' + parseFloat(price)
}
</wxs>
(2)实现页面样式
- 在pages/order/list/list.wxss文件中实现页面样式
/* pages/list/list.wxss */
page {
display: flex;
flex-direction: column;
height: 100%;
}
/* 折扣信息区 */
.discount {
width: 100%;
height: 70rpx;
line-height: 70rpx;
background: #fef9e6;
font-size: 28rpx;
text-align: center;
color: #999;
}
.discount-txt {
color: #fff;
padding: 5rpx 10rpx;
background: red;
margin-right: 15rpx;
}
.content {
flex: 1;
display: flex;
overflow: hidden;
}
.category {
width: 202rpx;
height: 100%;
background: #fcfcfc;
font-size: 28rpx;
}
/* 隐藏滚动条 */
::-webkit-scrollbar {
width: 0;
height: 0;
color: transparent;
}
.category-item {
height: 100rpx;
line-height: 100rpx;
text-align: center;
}
.food-category {
font-size: 24rpx;
background: #f3f4f6;
padding: 10rpx;
color: #ff9c35;
}
.food-item {
display: flex;
margin: 40rpx 20rpx;
}
.food-item-pic {
margin-right: 20rpx;
width: 94rpx;
height: 94rpx;
}
.food-item-pic > image {
width: 100%;
height: 100%;
}
.food-item-info {
flex: 1;
font-size: 30rpx;
margin-top: 4rpx;
}
.food-item-price {
margin-top: 14rpx;
color: #f05a86;
}
.food-item-opt {
margin-top: 40rpx;
}
.food-item-opt > i:before {
font-size: 44rpx;
color: #ff9c35;
content: "\e728";
}
.category-unselect {
color: #6c6c6c;
background: #f9f9f9;
border-bottom: 1rpx solid #e3e3e3;
}
.category-selected {
color: #ff9c35;
background: white;
border-left: 6rpx solid #ff9c35;
}
.category-selected:last-child {
border-bottom: 1rpx solid #e3e3e3;
}
/* 购物车区域 */
.operate {
height: 110rpx;
display: flex;
}
.operate-shopcart {
display: flex;
width: 74%;
padding: 10rpx;
background: #353535;
}
/* “选好了”按钮 */
.operate-submit {
width: 26%;
font-size: 30rpx;
background: #eee;
color: #aaa;
text-align: center;
line-height: 110rpx;
}
.operate-submit-activity {
background: #ff9c35;
color: #fff;
}
/* 购物车图标 */
.operate-shopcart-icon {
font-size: 80rpx;
color: #87888e;
margin-left: 20rpx;
position: relative;
}
.operate-shopcart-icon:before {
content: "\e73c";
}
.operate-shopcart-icon-activity {
color: #ff9c35;
}
/* 购物车为空 */
.operate-shopcart-empty {
color: #a9a9a9;
line-height: 88rpx;
font-size: 30rpx;
margin-left: 20rpx;
}
/* 购物车中的商品购买数量 */
.operate-shopcart-icon > span {
padding: 2rpx 14rpx;
border-radius: 50%;
background: red;
color: white;
font-size: 28rpx;
position: absolute;
top: 0px;
right: -10rpx;
text-align: center;
}
/* 购物车中的商品价格 */
.operate-shopcart-price {
display: flex;
}
.operate-shopcart-price > view {
font-size: 40rpx;
line-height: 88rpx;
margin-left: 25rpx;
color: #fff;
}
.operate-shopcart-price > text {
font-size: 24rpx;
line-height: 92rpx;
margin-left: 15rpx;
color: #aaa;
text-decoration: line-through;
}
/* 小球的样式 */
.operate-shopcart-ball {
width: 36rpx;
height: 36rpx;
position: fixed;
border-radius: 50%;
left: 50%;
top: 50%;
background: #ff9c35;
}
/* 满减优惠区域 */
.promotion {
padding: 7rpx 0 9rpx;
background: #ffcd9b;
color: #fff7ec;
font-size: 28rpx;
text-align: center;
}
.shopcart {
position: fixed;
top: 0;
left: 0;
bottom: 149rpx;
right: 0;
font-size: 28rpx;
}
.shopcart-wrap {
position: absolute;
width: 100%;
max-height: 90%;
bottom: 0;
background: #fff;
overflow: scroll;
}
.shopcart-mask {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: #000;
opacity: 0.5;
}
.shopcart-head {
position: fixed;
width: 100%;
background: #f0f0f0;
color: #878787;
line-height: 100rpx;
font-size: 26rpx;
overflow: hidden;
}
.shopcart-head-title {
float: left;
margin-left: 40rpx;
}
.shopcart-head-title:before {
background: #ff9c35;
width: 8rpx;
height: 32rpx;
content: "";
display: inline-block;
margin-right: 10rpx;
position: relative;
top: 6rpx;
}
.shopcart-head-clean {
float: right;
margin-right: 20rpx;
}
.shopcart-head-clean > i:before {
content: "\e61b";
position: relative;
top: 2rpx;
}
.shopcart-list {
margin-top: 101rpx;
}
.shopcart-item {
display: flex;
padding: 30rpx 20rpx;
line-height: 40rpx;
}
.shopcart-item > view {
margin-left: 30rpx;
}
.shopcart-item:not(:last-child) {
border-bottom: 1rpx solid #e3e3e3;
}
.shopcart-item-name {
flex: 1;
}
.shopcart-item-price {
color: #ff9c35;
}
.shopcart-item-number {
display: flex;
}
.shopcart-item-number > view {
margin: 0 15rpx;
}
.shopcart-icon-dec:before {
content: "\e61a";
font-size: 44rpx;
color: #888;
}
.shopcart-icon-add:before {
content: "\e728";
font-size: 44rpx;
color: #ff9c35;
}
(3)实现页面逻辑
- 在pages/order/list/list.js文件中实现页面逻辑
// pages/list/list.js
// 引入购物车动画模块
const shopcartAnimate = require('../../utils/shopcartAnimate.js')
const app = getApp()
const fetch = app.fetch
const categoryPosition = [] // 右列表各分类高度数组
Page({
data: {
foodList: [],
promotion: {},
activeIndex: 0,
tapIndex: 0,
cartPrice: 0, // 购物车中商品的总价格
cartNumber: 0, // 购物车中商品的总数量
cartList: {}, // 保存购物车数据
showCart: false,
},
disableNextScroll: false,
shopcartAnimate: null,
onLoad: function () {
wx.showLoading({
title: '努力加载中'
})
fetch('/food/list').then(data => {
wx.hideLoading()
this.setData({
foodList: data.list,
promotion: data.promotion[0]
}, () => {
var query = wx.createSelectorQuery()
var top = 0
var height = 0
query.select('.food').boundingClientRect(rect => {
top = rect.top
height = rect.height
})
query.selectAll('.food-category').boundingClientRect(res => {
res.forEach(rect => {
categoryPosition.push(rect.top - top - height / 3)
})
})
query.exec()
})
}, () => {
this.onLoad()
})
this.shopcartAnimate = shopcartAnimate('.operate-shopcart-icon', this)
},
tapCategory: function (e) {
this.disableNextScroll = true
var index = e.currentTarget.dataset.index
this.setData({
activeIndex: index,
tapIndex: index
})
},
onFoodScroll: function (e) {
if (this.disableNextScroll) {
this.disableNextScroll = false
return
}
var scrollTop = e.detail.scrollTop
var activeIndex = 0
categoryPosition.forEach((item, i) => {
if (scrollTop >= item) {
activeIndex = i
}
})
if (activeIndex !== this.data.activeIndex) {
this.setData({ activeIndex })
}
},
// 加入购物车
addToCart: function (e) {
const index = e.currentTarget.dataset.index
const category_index = e.currentTarget.dataset.category_index
const food = this.data.foodList[category_index].food[index]
const cartList = this.data.cartList
if (cartList[index]) {
++cartList[index].number
} else {
cartList[index] = {
id: food.id,
name: food.name,
price: parseFloat(food.price),
number: 1
}
}
this.setData({
cartList,
cartPrice: this.data.cartPrice + cartList[index].price,
cartNumber: this.data.cartNumber + 1
})
this.shopcartAnimate.start(e)
},
showCartList: function () {
if (this.data.cartNumber > 0) {
this.setData({
showCart: !this.data.showCart
})
}
},
cartNumberAdd: function(e) {
var id = e.currentTarget.dataset.id
var cartList = this.data.cartList
++cartList[id].number
this.setData({
cartList: cartList,
cartNumber: ++this.data.cartNumber,
cartPrice: this.data.cartPrice + cartList[id].price
})
},
cartNumberDec: function(e) {
var id = e.currentTarget.dataset.id
var cartList = this.data.cartList
if (cartList[id]) {
var price = cartList[id].price
if (cartList[id].number > 1) {
--cartList[id].number
} else {
delete cartList[id]
}
this.setData({
cartList: cartList,
cartNumber: --this.data.cartNumber,
cartPrice: this.data.cartPrice - price
})
if (this.data.cartNumber <= 0) {
this.setData({
showCart: false
})
}
}
},
// 清空购物车
cartClear: function() {
this.setData({
cartList: {},
cartNumber: 0,
cartPrice: 0,
showCart: false
})
},
// 实现跳转到订单确认页
order: function() {
if (this.data.cartNumber === 0) {
return
}
wx.showLoading({
title: '正在生成订单'
})
fetch('/food/order', {
order: this.data.cartList
}, 'POST').then(data => {
wx.navigateTo({
url: '/pages/order/checkout/checkout?order_id=' + data.order_id
})
}, () => {
this.order()
})
}
})
(4)实现页面效果
9、消费记录页
(1)实现页面结构
- 在pages/record/record.wxml文件中实现页面结构
<!--pages/record/record.wxml-->
<view class="head">
<button class="avatar-wrapper" open-type="chooseAvatar" bindchooseavatar="onChooseAvatar">
<image class="avatar" src="{{ avatarUrl }}" />
</button>
</view>
<view class="content">
<view class="list-title">消费记录</view>
<view class="list-item" wx:for="{{ list }}" wx:key="id">
<view class="list-item-l">
<view>消费</view>
<view class="list-item-time">{{ item.pay_time }}</view>
</view>
<view class="list-item-r">
<text>{{ priceFormat(item.price) }}</text>
</view>
</view>
</view>
<wxs module="priceFormat">
module.exports = function (price) {
return '¥ ' + parseFloat(price)
}
</wxs>
(2)实现页面样式
- 在pages/record/record.wxss文件中实现页面样式
/* pages/record/record.wxss */
page {
background-color: #f8f8f8;
font-size: 32rpx;
}
.head {
width: 100%;
background-color: #f7982a;
height: 400rpx;
display: flex;
justify-content: center;
align-items: center;
}
.avatar-wrapper {
width: 160rpx;
height: 160rpx;
padding: 0;
background: none;
border-radius: 50%;
}
.avatar {
width: 160rpx;
height: 160rpx;
border-radius: 50%;
}
/* 列表部分 */
.list-title {
background-color: #fff;
padding: 16rpx 0;
text-align: center;
}
.list-item {
display: flex;
padding: 42rpx 20rpx;
border-bottom: 1rpx solid #ececec;
}
.list-item-l {
flex: 1;
}
.list-item-r {
line-height: 76rpx;
}
.list-item-r > text {
color: #f7982a;
font-weight: 600;
font-size: 32rpx;
}
.list-item-time {
margin-top: 10rpx;
font-size: 26rpx;
color: #999;
}
(3)实现页面逻辑
- 在pages/record/record.js文件中实现页面逻辑
// pages/record/record.js
const defaultAvatar = '/images/avatar.png'
const app = getApp()
const fetch = app.fetch
Page({
data: {
avatarUrl: defaultAvatar
},
onLoad: function () {
wx.showLoading({
title: '努力加载中'
})
fetch('/food/record').then(data => {
wx.hideLoading()
this.setData(data)
})
},
onChooseAvatar: function (e) {
const { avatarUrl } = e.detail
this.setData({ avatarUrl })
}
})