# 第13次课 讲稿

> 课程：小程序开发
> 主题：综合项目——点餐系统（上）
> 教材：《微信小程序开发实战（第2版）》第6章
> 时长：3学时（理论 1 + 实践 2）
> 重点：项目架构设计、首页与菜单页开发
> 难点：项目架构规划、组件拆分

---

## 一、开场（0:00）

从今天开始进入第6章，做一个完整的项目——点餐系统。

前面十二次课，我们是"一个知识点一个案例"。今天开始不一样了：**没有新语法要学，全部是把学过的东西组织起来**。

我想先泼一点冷水。很多同学做小项目很顺，一到大项目就崩，原因不是技术不会，而是**没规划就开始写**。写到第五个页面才发现请求代码复制了五遍，改一个接口地址要改五个文件。

所以今天前半节课我们不写代码，先做三件事：**画页面结构、定目录结构、定接口规范**。这三件事想清楚了，后面两次课会顺很多。

（在白板上和学生一起列出点餐系统需要哪些页面）

---

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

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

- 掌握小程序项目的架构设计方法
- 理解组件化开发的思路
- 能够完成点餐系统首页与菜单页
- 案例：案例6 点餐系统(上)

本次课 3 学时的安排是：**理论讲解 1 学时，上机实践 2 学时**——项目架构设计讲解 1 学时，点餐系统上半部分实操 2 学时。

---

## 三、知识点讲解

### 幻灯片 3：项目架构设计

**【8:00 讲解】**

- 页面规划: 首页/菜单/购物车/订单/我的
- 目录结构: pages/ components/ utils/ api/ images/
- 请求封装: utils/request.js 统一管理
- 数据管理: 全局状态 vs 页面状态
- 组件拆分: 菜品卡片/购物车栏/分类导航

### 幻灯片 4：首页开发

**【13:00 讲解】**

- 顶部轮播图: swiper组件实现广告位
- 搜索框: input + 搜索按钮
- 分类导航: 横向滚动的分类图标
- 推荐菜品: 瀑布流/列表布局
- 底部tabBar: 首页/菜单/订单/我的

### 幻灯片 5：菜单页开发

**【18:00 讲解】**

- 左侧分类列表: 纵向滚动分类
- 右侧菜品列表: 根据分类动态加载
- 菜品卡片: 图片+名称+价格+加购按钮
- 分类切换: 点击左侧分类, 右侧滚动到对应位置
- 滚动联动: 右侧滚动时, 左侧分类高亮跟随

### 幻灯片 6：请求模块封装

**【23:00 讲解】**

- baseURL配置: 统一接口前缀
- 请求拦截: 添加token等认证信息
- 响应拦截: 统一错误处理
- API模块化: 按功能拆分(home/menu/order)
- async/await: 简化异步调用

---

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

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

### 项目架构

像装修房子。先出图纸（页面结构），再定水电走线（数据流），最后才刷墙买家具（写页面）。没人会先买沙发再想客厅在哪。

### 组件拆分的粒度

判断标准很简单：**同一个东西出现在两个以上地方，就该拆成组件**。菜品卡片在首页推荐、菜单列表、搜索结果里都出现，那它就该是组件；而"订单详情页顶部的那块状态栏"只出现一次，就不必拆。

### 请求封装

像公司的收发室。所有信件统一从收发室走，好处是：换地址只改一处、每封信自动盖公章（token）、有问题统一登记（错误处理）。每个人自己寄信，看起来省事，出问题时无从下手。

### 分类联动滚动

像翻词典。左边是字母索引，右边是正文。点索引跳到对应页（点分类 → 右侧滚到位置），手动翻页时索引也要跟着高亮（右侧滚动 → 左侧变色）。两个方向都要通，而且要防止互相触发形成死循环。

---

## 五、代码演示环节

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

### 演示 1：项目架构设计

**文件：** `project-structure.js`　**预期效果：** 项目架构概览

```js
// 点餐系统目录结构
// ┌─ pages/
// │  ├─ index/        首页（轮播+搜索+分类+推荐）
// │  ├─ menu/         菜单页（分类+菜品列表）
// │  ├─ cart/         购物车页
// │  ├─ order/        订单确认页
// │  └─ mine/         个人中心
// ├─ utils/
// │  ├─ request.js    请求封装
// │  └─ util.js       工具函数
// ├─ api/
// │  ├─ dish.js       菜品接口
// │  ├─ order.js      订单接口
// │  └─ user.js       用户接口
// └─ app.js

// app.json 页面配置
{
pages: [
"pages/index/index",
"pages/menu/menu",
"pages/cart/cart",
"pages/order/order",
"pages/mine/mine"
],
tabBar: {
color: "#999",
selectedColor: "#07c160",
list: [
{ pagePath: "pages/index/index", text: "首页", iconPath: "icons/home.png" },
{ pagePath: "pages/menu/menu", text: "菜单", iconPath: "icons/menu.png" },
{ pagePath: "pages/cart/cart", text: "购物车", iconPath: "icons/cart.png" },
{ pagePath: "pages/mine/mine", text: "我的", iconPath: "icons/mine.png" }
]
}
}

// app.js 全局数据
App({
globalData: {
userInfo: null,
cartList: [],  // 全局购物车
baseUrl: 'https://api.example.com'
}
})
```

**讲解口径：**

点餐系统项目架构：5个页面——**首页**展示轮播搜索分类推荐，**菜单页**左右分类联动，**购物车**管理商品，**订单页**确认提交，**个人中心**管理信息。工具层 **utils/request.js** 封装网络请求，**api/** 目录按模块拆分接口。app.json 配置 **tabBar** 底部导航4个标签页，app.js 的 **globalData** 存储全局购物车和用户信息。

### 演示 2：首页开发

**文件：** `index-page.wxml`　**预期效果：** 首页效果

```html
<!-- 首页 index.wxml -->
<view class="container">

<!-- 轮播图 -->
<swiper autoplay circular interval="3000"
indicator-dots indicator-color="rgba(0,0,0,0.3)"
style="height:360rpx">
<swiper-item wx:for="{{banners}}" wx:key="id">
<image src="{{item.image}}" mode="aspectFill"/>
</swiper-item>
</swiper>

<!-- 搜索栏 -->
<view class="search-bar">
<icon type="search" size="14"/>
<input placeholder="搜索菜品" bindinput="onSearch"/>
</view>

<!-- 分类导航 -->
<view class="category-grid">
<view class="category-item" wx:for="{{categories}}"
bindtap="goToMenu" data-id="{{item.id}}">
<image src="{{item.icon}}"/>
<text>{{item.name}}</text>
</view>
</view>

<!-- 推荐菜品 -->
<view class="recommend">
<view class="section-title">🔥 推荐菜品</view>
<view class="dish-grid">
<view class="dish-card" wx:for="{{recommendDishes}}"
bindtap="goToDetail" data-id="{{item.id}}">
<image src="{{item.image}}" mode="aspectFill"/>
<text class="dish-name">{{item.name}}</text>
<text class="dish-price">¥{{item.price}}</text>
</view>
</view>
</view>
</view>

// index.js 数据加载
Page({
data: { banners: [], categories: [], recommendDishes: [] },

onLoad() {
this.loadBanners()
this.loadCategories()
this.loadRecommend()
},

loadBanners() {
getBanners().then(res => {
this.setData({ banners: res.data })
})
}
})
```

**讲解口径：**

首页由四大模块组成：**swiper** 轮播图，设置 autoplay 自动播放、circular 循环、indicator-dots 指示点；**搜索栏**用 input 组件绑定 bindinput 事件；**分类导航**用 grid 布局展示图标和名称，点击跳转菜单页并传递分类 id；**推荐菜品**用双列网格展示图片、名称和价格。数据在 **onLoad** 中并行加载，通过封装的 API 方法获取。

### 演示 3：菜单页开发

**文件：** `menu-page.js`　**预期效果：** 菜单页效果

```js
<!-- 菜单页 menu.wxml -->
<view class="menu-container">
<!-- 左侧分类 -->
<scroll-view class="category-list" scroll-y
scroll-into-view="{{scrollIntoView}}">
<view class="category-item {{currentCategory==item.id?'active':''}}"
wx:for="{{categories}}" bindtap="switchCategory"
data-id="{{item.id}}" data-index="{{index}}">
{{item.name}}
</view>
</scroll-view>

<!-- 右侧菜品列表 -->
<scroll-view class="dish-list" scroll-y
scroll-into-view="{{scrollIntoView}}"
bindscroll="onDishScroll">
<view id="cat-{{item.id}}" wx:for="{{dishData}}">
<view class="dish-section-title">{{item.categoryName}}</view>
<view class="dish-item" wx:for="{{item.dishes}}" wx:for-item="dish">
<image src="{{dish.image}}"/>
<view class="dish-info">
<text>{{dish.name}}</text>
<text class="price">¥{{dish.price}}</text>
<view class="add-btn" bindtap="addToCart" data-dish="{{dish}}">+</view>
</view>
</view>
</view>
</scroll-view>
</view>

// menu.js 核心逻辑
Page({
data: { categories: [], dishData: [], currentCategory: 0, scrollIntoView: '' },

// 切换分类（左侧点击 → 右侧滚动）
switchCategory(e) {
const id = e.currentTarget.dataset.id
this.setData({
currentCategory: id,
scrollIntoView: `cat-${id}`
})
},

// 右侧滚动 → 左侧高亮联动
onDishScroll(e) {
// 根据滚动位置计算当前可见分类
const scrollTop = e.detail.scrollTop
// 遍历分类标题位置，找到当前分类
for (let i = this.sectionTops.length - 1; i >= 0; i--) {
if (scrollTop >= this.sectionTops[i]) {
this.setData({ currentCategory: this.data.categories[i].id })
break
}
}
}
})
```

**讲解口径：**

菜单页采用左右双栏布局：**左侧 scroll-view** 显示分类列表，点击分类通过 **scroll-into-view** 控制右侧滚动到对应区域；**右侧 scroll-view** 显示菜品列表，按分类分组，每组有标题和菜品卡片。关键实现**滚动联动**：左侧点击设置 scrollIntoView 驱动右侧滚动，右侧滚动时通过 bindscroll 事件获取 scrollTop，遍历分类位置数组反推当前分类并高亮左侧。

### 演示 4：请求模块封装

**文件：** `request.js`　**预期效果：** 请求封装架构

```js
// utils/request.js 请求封装
const BASE_URL = 'https://api.example.com'

const request = (options) => {
return new Promise((resolve, reject) => {
wx.request({
url: BASE_URL + options.url,
method: options.method || 'GET',
data: options.data || {},
header: {
'content-type': 'application/json',
'Authorization': wx.getStorageSync('token') || ''
},
success: (res) => {
if (res.statusCode === 200) {
if (res.data.code === 0) {
resolve(res.data)
} else {
wx.showToast({ title: res.data.msg, icon: 'none' })
reject(res.data)
}
} else if (res.statusCode === 401) {
wx.showToast({ title: '请先登录' })
wx.navigateTo({ url: '/pages/login/login' })
}
},
fail: (err) => {
wx.showToast({ title: '网络异常', icon: 'none' })
reject(err)
}
})
})
}

// api/dish.js 菜品接口
import { request } from '../utils/request'

export const getDishList = (categoryId) =>
request({ url: '/dish/list', data: { categoryId } })

export const getDishDetail = (id) =>
request({ url: `/dish/detail/${id}` })

// api/order.js 订单接口
export const createOrder = (data) =>
request({ url: '/order/create', method: 'POST', data })

export const getOrderList = () =>
request({ url: '/order/list' })
```

**讲解口径：**

请求模块封装：**request.js** 用 Promise 封装 wx.request，自动拼接 **BASE_URL**，header 中自动携带 **token**。统一处理响应：code 为 0 表示成功 resolve，非0提示错误信息，**401** 状态跳转登录页，网络异常统一提示。API 按模块拆分到 **api/dish.js**、**api/order.js**、**api/user.js**，每个方法只需传入 url、method 和 data，调用简洁。

---

## 六、常见错误与排查

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

| # | 现象 | 原因与解决 |
|:-:|------|------------|
| 1 | 一上手就建 pages 写页面 | 先定目录结构（pages / components / utils / api / images），后面才不会乱。 |
| 2 | 每个页面各写一遍 wx.request | 必须先封装 utils/request.js。这一步在第7次课的挑战任务里已经练过了，正好用上。 |
| 3 | 把所有状态塞进 globalData | globalData 没有响应式，改了页面不会自动更新。只放真正全局的东西（用户信息、token），业务数据仍走页面 data。 |
| 4 | 分类联动写成死循环 | 左侧点击触发右侧滚动，右侧滚动又反过来改左侧高亮，然后再触发滚动。解法是加一个"当前是否为程序触发滚动"的标志位。 |
| 5 | 图片没压缩导致首屏很慢 | 菜品图直接用几 MB 的原图，列表一拉就卡。上 CDN 并按显示尺寸裁剪。 |

---

## 七、上机实践指引

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

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

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

---

## 八、本课小结

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

- 项目架构设计
- 首页开发
- 菜单页开发
- 请求模块封装

今天的产出不是一堆代码，而是**一份想清楚了的方案 + 跑通的首页和菜单页**。

我特别看重挑战任务里的"封装请求模块"。这个模块下次课会一直用，封装得好不好，直接决定下次课你是轻松还是痛苦。

回去之前，把你规划的页面结构和目录结构截图发到群里，我们下次课开始前花五分钟互相看看。

---

## 九、布置作业

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

### 【基础】搭建项目框架

按照项目目录结构创建点餐系统项目，配置app.json和tabBar

### 【进阶】完成登录与首页

实现登录模块（wx.login + Cookie自动登录）和首页模块（分类切换 + 菜品列表）

### 【挑战】封装请求模块

完善fetch模块，添加请求拦截、错误统一处理、loading状态管理、请求重试机制

---

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