# 第14次课 讲稿

> 课程：小程序开发
> 主题：综合项目——点餐系统（下）
> 教材：《微信小程序开发实战（第2版）》第6章
> 时长：3学时（理论 0.5 + 实践 2.5）
> 重点：购物车、订单、个人中心开发
> 难点：购物车状态管理、订单流程

---

## 一、开场（0:00）

上次课我们搭好了架子，做完了首页和菜单页。今天把剩下的补齐：购物车、订单、个人中心。

今天真正的难点只有一个：**购物车的状态放在哪里**。

想一想这个场景：用户在菜单页加了三个菜，切到购物车页要看到这三个菜，切回菜单页那三个菜的数量角标还得在，退出小程序再进来最好还在。这个数据同时被多个页面读写，还要持久化。

（让学生先说说自己的方案，再逐个分析利弊）

这就是所谓的"状态管理"问题。我们这个项目规模不大，用 globalData + storage 就够；但你要知道，项目再大一点就会需要 MobX 这类方案，原因就是从这里来的。

---

## 二、明确本课目标（约 5:00）

把目标写在白板上或直接翻到课件第 2 页，逐条念一遍：

- 掌握购物车的状态管理与交互
- 理解订单的完整流程
- 能够完成点餐系统全部功能
- 案例：案例6 点餐系统(下)

本次课 3 学时的安排是：**理论讲解 0.5 学时，上机实践 2.5 学时**——以项目实战为主，仅用 0.5 学时讲订单流程要点。

---

## 三、知识点讲解

### 幻灯片 3：购物车功能

**【8:00 讲解】**

- 添加商品: 点击加购按钮, 数量+1
- 数量修改: 加减按钮控制商品数量
- 全选/取消全选: 复选框联动
- 合计金额: 实时计算选中商品总价
- 删除商品: 左滑删除或长按删除
- 购物车数据: 本地缓存 vs 全局状态

### 幻灯片 4：订单功能

**【13:00 讲解】**

- 确认订单: 展示选中商品、金额、备注
- 收货地址: wx.chooseAddress 获取微信地址
- 提交订单: 调用后端接口创建订单
- 订单列表: 按状态分类(待付款/待收货/已完成)
- 订单详情: 展示订单完整信息

### 幻灯片 5：个人中心

**【18:00 讲解】**

- 用户信息: 头像/昵称展示
- 我的订单: 快捷入口(待付款/待收货/退款/全部)
- 收货地址管理: 增删改查
- 设置页面: 关于/清除缓存/退出登录
- 登录态管理: token存储与过期处理

### 幻灯片 6：项目优化与总结

**【23:00 讲解】**

- 性能优化: setData优化、图片懒加载、分包加载
- 体验优化: 骨架屏、加载动画、空状态
- 代码规范: ESLint、命名规范、注释
- 测试: 功能测试、兼容性测试
- 发布: 体验版 → 审核版 → 线上版

---

## 四、难点突破：用类比讲清楚

下面这几个点是学生最容易卡住的地方，抽象讲不通就换成类比。

### 购物车状态

像超市的手推车。车是跟着人走的（全局），不是跟着货架走的（页面）。所以它不能存在菜单页的 data 里，得存在一个"人"身上——globalData 或 storage。

### 订单状态流转

像快递单号的状态：待付款 → 已付款 → 备餐中 → 已完成，只能往前走，不能跳，也不能倒退。代码里最好把它定义成常量枚举，而不是到处写字符串"待付款"，否则写错一个字就查半天。

### 金额计算

永远不要用浮点数算钱。0.1 + 0.2 在 JavaScript 里不等于 0.3，这在课堂上一定要现场演示一遍。正确做法是把单位换成分（整数）来算，显示时再除以 100。

### 骨架屏

像饭店上菜前先给你摆好餐具。数据还在加载时先显示灰色的占位块，用户感知的等待时间会短很多——虽然实际时间一样。

---

## 五、代码演示环节

以下每一段都建议**现场敲**，不要直接贴完整代码。边敲边问"下一行该写什么"，让学生跟上节奏。

### 演示 1：购物车功能

**文件：** `cart.js`　**预期效果：** 购物车效果

```js
// 购物车核心逻辑 cart.js
const app = getApp()

Page({
data: { cartList: [], totalPrice: 0, totalCount: 0, isAllChecked: true },

onShow() {
this.refreshCart()
},

// 刷新购物车数据
refreshCart() {
const cartList = app.globalData.cartList
const totalPrice = cartList.reduce((sum, item) => {
return item.checked ? sum + item.price * item.count : sum
}, 0)
const totalCount = cartList.reduce((sum, item) => {
return item.checked ? sum + item.count : sum
}, 0)
this.setData({ cartList, totalPrice, totalCount })
},

// 添加商品（菜单页调用）
addToCart(dish) {
const cartList = app.globalData.cartList
const exist = cartList.find(item => item.id === dish.id)
if (exist) {
exist.count++  // 已存在则数量+1
} else {
cartList.push({ ...dish, count: 1, checked: true })
}
this.refreshCart()
},

// 数量增减
changeCount(e) {
const { id, type } = e.currentTarget.dataset
const item = app.globalData.cartList.find(i => i.id === id)
if (type === 'add') item.count++
if (type === 'minus') {
if (item.count 1) {
this.removeItem(id); return
}
item.count--
}
this.refreshCart()
},

// 删除商品
removeItem(id) {
app.globalData.cartList = app.globalData.cartList.filter(i => i.id !== id)
this.refreshCart()
},

// 全选/取消全选
toggleAllCheck() {
const isAllChecked = !this.data.isAllChecked
app.globalData.cartList.forEach(item => item.checked = isAllChecked)
this.refreshCart()
}
})
```

**讲解口径：**

购物车核心逻辑：数据存在 **app.globalData.cartList** 全局共享。**addToCart** 添加商品时先 find 查找是否已存在，存在则 count++，不存在则 push 新项。**changeCount** 数量增减，减到1再减则 **removeItem** 删除。**refreshCart** 用 **reduce** 计算选中商品总价和总数，全选按钮 toggleAllCheck 切换所有商品 checked 状态。页面 onShow 时刷新数据。

### 演示 2：订单功能

**文件：** `order.js`　**预期效果：** 订单确认页效果

```js
// 订单确认页 order.js
const app = getApp()
import { createOrder } from '../../api/order'

Page({
data: {
checkedItems: [], totalPrice: 0,
address: null, remark: ''
},

onLoad() {
// 筛选选中商品
const checkedItems = app.globalData.cartList.filter(item => item.checked)
const totalPrice = checkedItems.reduce((sum, i) => sum + i.price * i.count, 0)
this.setData({ checkedItems, totalPrice })
},

// 选择收货地址
chooseAddress() {
wx.chooseAddress({
success: (res) => {
this.setData({ address: res })
// res包含: userName, telNumber, provinceName...
}
})
},

// 提交订单
submitOrder() {
if (!this.data.address) {
return wx.showToast({ title: '请选择地址', icon: 'none' })
}
wx.showLoading({ title: '提交中...' })
createOrder({
items: this.data.checkedItems.map(i => ({
dishId: i.id, count: i.count, price: i.price
})),
address: this.data.address,
totalPrice: this.data.totalPrice,
remark: this.data.remark
}).then(res => {
wx.hideLoading()
// 清除已下单的购物车商品
app.globalData.cartList = app.globalData.cartList.filter(i => !i.checked)
wx.navigateTo({ url: '/pages/orderSuccess/orderSuccess?id=' + res.data.orderId })
}).catch(() => {
wx.hideLoading()
})
}
})

// ⚠️ 订单要点
// 1. chooseAddress 需用户授权
// 2. 提交前校验地址和商品
// 3. 下单成功后清除已选购物车项
// 4. 防重复提交：提交时禁用按钮
```

**讲解口径：**

订单确认页：onLoad 时 **filter** 筛选购物车中 checked 的商品，**reduce** 计算总价。**wx.chooseAddress** 调用微信地址选择器，返回 userName、telNumber、provinceName 等字段，需用户授权。**submitOrder** 提交订单前校验地址，showLoading 防止重复点击，调用 **createOrder** API 提交，成功后清除已下单的购物车商品并跳转成功页。注意防重复提交需在提交时禁用按钮。

### 演示 3：个人中心

**文件：** `mine.js`　**预期效果：** 个人中心效果

```js
// 个人中心 mine.js
const app = getApp()

Page({
data: {
userInfo: null,
orderStats: { pending: 0, paid: 0, done: 0 }
},

onShow() {
this.refreshUserInfo()
this.loadOrderStats()
},

// 登录/刷新用户信息
refreshUserInfo() {
const userInfo = app.globalData.userInfo
if (userInfo) {
this.setData({ userInfo })
}
},

// 登录操作
handleLogin() {
wx.login({ success: (res) => {
wx.getUserProfile({
desc: '用于完善会员资料',
success: (profileRes) => {
app.globalData.userInfo = profileRes.userInfo
this.setData({ userInfo: profileRes.userInfo })
}
})
}})
},

// 加载订单统计
loadOrderStats() {
getOrderStats().then(res => {
this.setData({ orderStats: res.data })
})
},

// 订单快捷入口
goToOrders(e) {
const tab = e.currentTarget.dataset.tab
wx.navigateTo({ url: `/pages/orderList/orderList?tab=${tab}` })
},

// 地址管理
goToAddress() {
wx.navigateTo({ url: '/pages/addressList/addressList' })
}
})

<!-- mine.wxml 关键结构 -->
<view class="user-card" wx:if="{{userInfo}}">
<image src="{{userInfo.avatarUrl}}" class="avatar"/>
<text>{{userInfo.nickName}}</text>
</view>
<view class="login-btn" wx:else bindtap="handleLogin">
点击登录
</view>
```

**讲解口径：**

个人中心页面：顶部用户卡片，未登录显示 **登录按钮**，已登录显示头像和昵称。登录用 **wx.login + getUserProfile** 获取信息存入 globalData。中部订单快捷入口，显示待付款、待配送、已完成数量，点击跳转对应订单列表。下部功能菜单包括 **收货地址管理**、联系客服、设置等。页面 onShow 时刷新用户信息和订单统计数据。

### 演示 4：项目优化与总结

**文件：** `optimization.js`　**预期效果：** 优化与发布流程

```js
// 1. 骨架屏优化首屏体验
<!-- skeleton.wxml -->
<view wx:if="{{loading}}">
<view class="skeleton-banner"></view>
<view class="skeleton-rect"></view>
<view class="skeleton-circle"></view>
</view>
<view wx:else>...真实内容...</view>

/* 骨架屏样式 */
.skeleton-banner { background: #f0f0f0; height: 360rpx; }
.skeleton-rect { background: #f0f0f0; height: 40rpx; margin: 20rpx; border-radius: 8rpx; }

// 2. setData 性能优化
// ❌ 频繁调用 setData
this.setData({ name: 'A' })
this.setData({ age: 20 })
// ✅ 合并调用
this.setData({ name: 'A', age: 20 })

// ❌ 传递大量不需要的数据
this.setData({ list: hugeArray })
// ✅ 只更新变化的部分
this.setData({ 'list[0].count': 2 })

// 3. 图片优化
// 使用 mode="aspectFill" 避免变形
// 使用 lazy-load 懒加载
<image mode="aspectFill" lazy-load />

// 4. 代码规范
// • 统一命名：页面小写，组件驼峰
// • 公共逻辑提取到 utils
// • 请求统一走 request.js 封装
// • 及时清除定时器和监听器

// 5. 发布流程
// ① 上传代码 → ② 提交审核 → ③ 发布上线
// 开发版 → 体验版 → 审核版 → 线上版
```

**讲解口径：**

项目优化要点：**骨架屏** 在数据加载前显示灰色占位元素，提升首屏感知速度；**setData 优化**——合并多次调用为一次，只更新变化的数据路径如 **'list[0].count'** 而非整个数组；**图片优化**用 aspectFill 防变形、lazy-load 懒加载。代码规范：统一命名、公共逻辑提取 utils、请求统一走封装。发布流程：上传代码→体验版测试→提交审核→发布上线。

---

## 六、常见错误与排查

上机环节学生一定会遇到下面这些问题，提前讲一遍能省掉大量重复答疑。

| # | 现象 | 原因与解决 |
|:-:|------|------------|
| 1 | 总价算出 19.900000000000002 | 浮点数精度问题。用整数分计算，或者用 toFixed(2) 处理显示（但计算过程仍要用整数）。 |
| 2 | 购物车切页面就清空 | 状态存在了页面 data 里。改存 globalData，并在关键节点同步到 storage。 |
| 3 | 全选逻辑写反 | "全选"的状态应该由"是否所有项都被选中"推导出来，而不是独立维护一个布尔值——否则用户手动取消一项时，全选框不会自动变化。 |
| 4 | 重复提交订单 | 提交按钮点两下下了两单。提交前立刻禁用按钮并加锁，成功或失败后再恢复。 |
| 5 | chooseAddress 调用失败 | 该接口需要在后台开通并声明用户隐私保护指引，新项目要按最新合规要求配置。 |
| 6 | token 过期后所有请求都失败 | request 封装里要统一拦截 401，自动重新登录并重发一次原请求。 |

---

## 七、上机实践指引

本次课对应教材案例 1 个，源码已放在站点 `code/session14/` 下：

- **【案例6】综合项目 点餐系统** — 含 项目实现、服务器端 共 2 个工程

建议顺序：先带学生跑一遍「知识储备」里的最小示例，确认每个知识点都能单独跑通，再进「案例实现」做完整案例。直接上完整案例，出错了不知道错在哪。

---

## 八、本课小结

回到课件小结页，逐条回顾：

- 购物车功能
- 订单功能
- 个人中心
- 项目优化与总结

到这里，点餐系统的完整链路走通了：浏览 → 加购 → 下单 → 查看订单。这是你们第一个"能给别人看"的作品。

第6章结束后，我建议每个人把这个项目留着，稍加打磨就是很好的求职作品。下次课我们讲进阶内容和跨平台开发，让这个项目变得更专业。

作业里的"完整联调与优化"，重点做三件事：性能评分跑一遍、加骨架屏、把 console.log 清干净。

---

## 九、布置作业

三档任务，基础必做，进阶建议做，挑战选做。

### 【基础】完成购物车功能

实现购物车的商品增删、数量修改、总价计算功能

### 【进阶】完成订单管理

实现订单提交、订单列表、订单详情功能，完成多页面数据同步

### 【挑战】完整联调与优化

完成点餐系统所有功能联调，优化用户体验（加载状态、空状态提示、错误处理）

---

> 配套资源：课件 `slides-v2/session14.pptx`　课堂视频 `videos-output/session14.mp4`　扩展阅读 `docs/第14次课_扩展阅读.md`
