当前位置: 首页 > article >正文

微信小程序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": "我的"
  }]
}, 
  • 目录结构如下

image-20240916161740080

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)实现页面效果

image-20240916214516902

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)实现页面效果

image-20240916214950744

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)实现页面效果

image-20240916215430458

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)实现页面效果

image-20240917162105360

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 })
  }
})
(4)实现页面效果

image-20240917164247314


http://www.kler.cn/a/319250.html

相关文章:

  • MaxKB
  • Python Plotly 库使用教程
  • 任何使用 Keras 进行迁移学习
  • 【AI日记】24.11.14 复习和准备 RAG 项目 | JavaScript RAG Web Apps with LlamaIndex
  • PyTorch深度学习与企业级项目实战-预训练语言模型GPT
  • WebGIS三维地图框架--Cesium
  • 京准电钟:NTP网络校时服务器助力校园体育场馆
  • MySQL 8.0 与 8.4 主主同步
  • E+H超声波物位仪FMU42-ATB2A22A
  • 企业内训|华为昇腾智算中心深度技术研修-某智算厂商研发中心
  • mysql RR是否会导致幻读?
  • [Redis][事务]详细讲解
  • 激光粉尘传感器:筑牢粮仓安全防线,有效应对粮食粉尘爆炸高危风险
  • 为什么一定要学AI(Stable Diffusion)做设计? 会用AI和不会用AI的区别真的太大了!打工人一定要看!
  • SQL中的WITH AS语法
  • Java项目中异常处理的最佳实践
  • nginx+keepalived健康检查案例详解(解决nginx出现故障却不能快速切换到备份服务器的问题)
  • 【Java特性】多态详解——对象类型转换与 instanceof 关键字的运用
  • python使用vscode 所需插件
  • VCS和Verdi联合仿真使用学习记录
  • 哪里有同等学力申硕英语历年真题答案
  • 计算机知识科普问答--17(81-85)
  • pg入门5—pg有哪些系统schema
  • git add成功后忘记commit的文件丢了?
  • Linux、Windows、Android下查看可执行文件、动态库和静态库信息的命令
  • 数据结构|二叉搜索树