ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

UniApp实战:从零开发奶茶店点餐微信小程序全流程

UniApp实战:从零开发奶茶店点餐微信小程序全流程 点了几十次外卖奶茶之后我决定自己写一个奶茶店点餐小程序。真正动手做下来才发现这个项目比想象中更适合作为一套完整的前端实战案例——商品展示、规格组合甜度、冰量、加料、购物车、订单提交、微信支付、门店自取/配送电商该有的核心链路全占了。更关键的是我选了UniApp来做这套系统一套代码编译到微信小程序顺带还能出H5和App项目价值一下就上来了。这篇文章我会按实际开发顺序把整个奶茶店点餐系统的设计思路、技术选型、关键代码、数据库设计和上线配置完整过一遍特别是那些常规文档里不会写的坑直接给你避雷。想练手UniApp、做毕设或者想把点餐类小程序做扎实的开发者这篇文章都能给你一套可直接抄作业的方案。1. 需求拆解与技术选型先把“做什么”想清楚1.1 奶茶点餐到底要解决哪些问题很多人一上来就写页面结果做到一半发现购物车、规格、支付全乱套。我建议第一步先把需求拆清楚奶茶店点餐系统本质上是一个“商品浏览-加购-下单-支付-履约”的完整闭环角色主要分用户端和门店端。用户端的核心诉求很明确快速浏览菜单、按分类筛选商品、选择规格糖度、冰量、加料、加入购物车、提交订单、微信支付、查看订单状态、到店取餐或者等外卖配送。这里面最容易被忽略的就是“规格组合”奶茶和普通电商商品最大的区别就在这里——同一杯杨枝甘露可以有“少冰、半糖、加椰果”和“去冰、全糖、不加料”完全不同的SKU。如果商品模型设计不到位后续购物车和库存逻辑会很难受。门店端的诉求相对简单收到新订单提醒、更新制作状态、处理自取/外送订单、查看当日营收。如果这是毕业设计或学习项目做一套简易的商家管理端就够了不需要单独开发一个完整后台系统用微信公众号后台或者一个简单的Web管理页都能实现。我用一张需求优先级表来梳理功能模块功能点优先级说明用户端商品分类与列表高左侧分类、右侧商品列表用户端规格选择弹窗高糖度/冰量/加料 组合用户端购物车高悬浮条弹层实时计算用户端确认订单高门店选择、自取/外送用户端微信支付高uni.requestPayment用户端订单列表/详情中状态流转展示门店端订单管理中接单、制作、完成门店端商品管理低简单CRUD即可1.2 为什么选UniApp而不是原生微信小程序这是很多新手纠结的问题。我个人的结论是如果你只做微信小程序原生小程序完全够用但如果想一套代码同时覆盖微信小程序、支付宝小程序、H5和AppUniApp是当前综合成本最低的方案。UniApp最大的优势是使用Vue语法开发前端人员上手非常快而且通过HBuilderX可以一键运行到微信开发者工具调试效率比原生小程序高不少。另一个好处是周边的UI组件库很成熟比如uview-plus、uni-ui像商品卡片、弹出层、数量选择器这些都有现成组件省掉大量造轮子的时间。当然它也有坑UniApp做了跨端兼容层有些微信小程序特有的能力比如某些微信原生API、特定组件需要通过条件编译来处理另外如果项目非常复杂、页面很多它打包出来的体积会比原生小程序大一些。对奶茶点餐这种中轻量项目来说UniApp完全够用而且还能在毕设答辩或者项目展示时多一个“跨端”的亮点。1.3 整体架构与技术栈前端页面后端接口数据库一个真实的点餐系统前端只是冰山一角完整架构应该是前端页面后端API数据库三部分串联。我的技术栈选型是这样的前端UniAppVue3 PiniaUI组件用uview-plus后端Node.js Express也可以用Java Spring Boot或PHP ThinkPHP根据自己熟悉程度来数据库MySQL 8.0文件存储商品图片用对象存储或本地静态目录登录鉴权微信登录 code2Session 自定义token整体请求链路是小程序页面 → uni.request → 后端API → MySQL数据库 → 返回JSON数据 → 前端渲染。这里我特别想强调一下微信小程序要求所有请求域名必须是HTTPS且已备案而且要在小程序管理后台配置合法域名这一点开发阶段容易忽略后面上线部分我会详细讲。后端接口我按业务模块划分主要包含这些商品类接口分类列表、商品列表、商品详情、购物车接口这里其实可以纯前端实现不进数据库、订单接口创建订单、订单列表、订单详情、订单状态变更、支付接口统一下单、支付回调、登录接口微信登录、token校验。这些接口合起来大概20个左右足够支撑一个完整的点餐流程。2. 数据库与页面结构设计底子打好后面才不返工2.1 数据表设计商品、规格、订单三张核心表数据库设计是整个项目的基石我第一版做得比较随意结果后面订单统计非常痛苦所以第二版专门重构了这几张表。核心表主要就是商品分类表、商品表、规格表、门店表、订单表、订单明细表。先看商品分类表和商品表这个比较简单CREATE TABLE goods_category ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(50) NOT NULL COMMENT 分类名, sort int(11) DEFAULT 0 COMMENT 排序, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE goods ( id int(11) NOT NULL AUTO_INCREMENT, category_id int(11) NOT NULL COMMENT 分类ID, name varchar(100) NOT NULL COMMENT 商品名, main_image varchar(255) NOT NULL COMMENT 主图, price decimal(10,2) NOT NULL COMMENT 基础价格, description varchar(500) DEFAULT , month_sales int(11) DEFAULT 0 COMMENT 月售, status tinyint(1) DEFAULT 1 COMMENT 1上架 0下架, sort int(11) DEFAULT 0, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;规格表是重点。奶茶的规格有两类一类是“维度”比如糖度、冰量、加料另一类是“可选项”比如糖度可选“全糖、半糖、三分糖、无糖”冰量可选“常规冰、少冰、去冰、热”。我最终采用的方案是把每个维度的可选项用JSON存字段里简洁直观CREATE TABLE goods_spec ( id int(11) NOT NULL AUTO_INCREMENT, goods_id int(11) NOT NULL COMMENT 商品ID, spec_name varchar(50) NOT NULL COMMENT 维度名如糖度/冰量/加料, spec_options json NOT NULL COMMENT 可选项如[全糖,半糖,三分糖,无糖], is_required tinyint(1) DEFAULT 1 COMMENT 是否必选, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;为什么用JSON因为不同商品的可选项差异很大硬拆成独立表反而复杂。JSON字段在MySQL 8.0里可以很方便地用JSON_CONTAINS等函数查询对这类场景足够用了。如果以后要做库存管理可以再单独设计SKU表但那是供应链系统的复杂度点餐小程序这个阶段用不上。订单表和订单明细表我用两张CREATE TABLE orders ( id int(11) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单号, user_id int(11) NOT NULL, store_id int(11) NOT NULL COMMENT 门店ID, total_amount decimal(10,2) NOT NULL COMMENT 总金额, pay_amount decimal(10,2) NOT NULL COMMENT 实付金额, pay_status tinyint(1) DEFAULT 0 COMMENT 0未支付 1已支付 2已退款, order_status tinyint(1) DEFAULT 0 COMMENT 0待支付 1制作中 2待取餐 3配送中 4已完成 5已取消, pickup_type tinyint(1) DEFAULT 0 COMMENT 0自取 1外送, remark varchar(255) DEFAULT COMMENT 备注, create_time datetime DEFAULT CURRENT_TIMESTAMP, pay_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE order_items ( id int(11) NOT NULL AUTO_INCREMENT, order_id int(11) NOT NULL, goods_id int(11) NOT NULL, goods_name varchar(100) NOT NULL, spec_desc varchar(255) DEFAULT COMMENT 规格描述如少冰半糖椰果, price decimal(10,2) NOT NULL, quantity int(11) NOT NULL DEFAULT 1, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;订单明细里一定要存商品名称和规格描述的快照因为商品价格和名称以后可能会改但用户历史订单里必须保留当时的购买信息。2.2 页面路由规划tabBar设计、购物车放在哪页面结构直接决定用户体验。我的设计延续了主流奶茶小程序的交互习惯底部TabBar只有三个入口——首页点单、订单、我的购物车不单独占一个Tab而是做成首页底部的悬浮条点击弹出购物车层。这个设计是有讲究的奶茶店点餐和电商购物不一样用户心智是“快速选中、快速结算”把购物车放在首页底部用户加购后不用跳页就能看到数量可以继续加购体验更流畅。如果单独设一个购物车Tab反而打断了点单节奏。pages.json里主要的页面配置是这样的{ pages: [ { path: pages/index/index, style: { navigationBarTitleText: 点单, enablePullDownRefresh: false } }, { path: pages/order/order, style: { navigationBarTitleText: 订单 } }, { path: pages/my/my, style: { navigationBarTitleText: 我的 } }, { path: pages/confirm/confirm, style: { navigationBarTitleText: 确认订单 } }, { path: pages/order/detail, style: { navigationBarTitleText: 订单详情 } } ], tabBar: { color: #999, selectedColor: #e54d42, list: [ { pagePath: pages/index/index, text: 点单 }, { pagePath: pages/order/order, text: 订单 }, { pagePath: pages/my/my, text: 我的 } ] } }商品详情页我没单独做路由而是用商品列表页的弹出层popup来实现规格选择这更符合奶茶点餐“不离开菜单”的使用习惯。2.3 接口设计与状态管理请求封装、登录态、购物车持久化前端项目的工程质量看请求封装就知道了。我在utils/request.js里统一封装了uni.request基础URL、token注入、错误码处理都集中在这里// utils/request.js const BASE_URL https://yourdomain.com/api export function request({ url, method GET, data {} }) { return new Promise((resolve, reject) { uni.request({ url: BASE_URL url, method, data, header: { Content-Type: application/json, Authorization: uni.getStorageSync(token) || }, success: (res) { if (res.statusCode 401) { // token过期跳转登录 uni.navigateTo({ url: /pages/login/login }) reject(res) return } if (res.statusCode 200 res.data.code 0) { resolve(res.data.data) } else { uni.showToast({ title: res.data.msg || 请求失败, icon: none }) reject(res) } }, fail: (err) { uni.showToast({ title: 网络异常, icon: none }) reject(err) } }) }) }登录态的流程是用户点击微信登录 → 前端调用 uni.login 获取code → 传给后端 → 后端调用微信接口 code2Session 拿到openid → 后端生成自定义token返回前端 → 前端把token存到uni.setStorageSync。后续所有接口都带这个token后端通过中间件校验。微信小程序的code五分钟有效且只能用一次后端需要处理好校验逻辑多次使用同一个code要报错处理。购物车状态我用Pinia管理并且把状态持久化到本地存储。这样用户退出小程序再进来购物车数据还在。核心的store逻辑后面第三部分会细讲。3. 核心功能实现从菜单浏览到支付完成的完整链路3.1 菜单页与规格弹窗甜度、冰量、加料怎么选菜单页布局分两块左侧是一级分类招牌、奶茶、果茶、纯茶、加料右侧是当前分类下的商品列表。左侧分类切换后右侧商品列表更新。这里有个小技巧商品列表用scroll-view做分栏滚动左侧分类点击时右侧滚动到对应分组。如果分类和商品数据都从接口拿要注意切换分类时加上loading状态避免白屏闪烁。商品卡片包括图片、名称、描述、月售、价格和“加入购物车”按钮。点击加号后弹出规格选择层这个层是整个项目交互最核心的部分。规格选择的实现思路从后端拿到该商品的规格维度列表[{spec_name: 糖度, spec_options: [全糖,半糖,三分糖,无糖]}, {spec_name: 冰量, spec_options: [常规冰,少冰,去冰,热]}, {spec_name: 加料, spec_options: [椰果,珍珠,芋圆,布丁]}]然后前端遍历渲染每个维度用户每选完一个维度就记录选项最后形成一条完整的specDesc字符串比如“少冰、半糖、加椰果”。这里要特别注意一个逻辑加料维度允许多选要单独处理其它维度是单选。我给每个选项加了一个selected状态加料点击时用toggle其它维度点击时替换当前维度的选中值。选完后点“加入购物车”把商品ID、specDesc、价格、数量一并传给购物车store。规格弹窗的代码大致是这样的结构template uni-popup refspecPopup typebottom view classspec-panel view classgoods-info image :srccurrentGoods.main_image / view text classprice¥{{ currentGoods.price }}/text text classsales月售{{ currentGoods.month_sales }}/text /view /view view v-for(specGroup, idx) in specList :keyidx classspec-group text classspec-name{{ specGroup.spec_name }}/text view classspec-options view v-for(opt, optIdx) in specGroup.spec_options :keyoptIdx :class[spec-option, { active: isSelected(specGroup.spec_name, opt, idx) }] clicktoggleOption(specGroup, opt) {{ opt }} /view /view /view button classadd-btn clickaddToCart加入购物车/button /view /uni-popup /template3.2 购物车逻辑数量加减法规格相同才合并购物车是点餐系统最容易出bug的地方核心难点不是加减数量而是“合并”细节。我的购物车store里每个购物车项的key由商品IDspecDesc组合形成比如“12-少冰/半糖/椰果”只有完全相同的规格才合并数量不同规格必须分开。Pinia里的购物车store大概是这样// stores/cart.js import { defineStore } from pinia export const useCartStore defineStore(cart, { state: () ({ items: uni.getStorageSync(cartItems) || [] }), getters: { totalCount: (state) state.items.reduce((sum, item) sum item.quantity, 0), totalPrice: (state) state.items.reduce((sum, item) sum item.price * item.quantity, 0) }, actions: { addItem(goods, specDesc, price, quantity 1) { const key ${goods.id}-${specDesc} const existing this.items.find(item item.key key) if (existing) { existing.quantity quantity } else { this.items.push({ key, goodsId: goods.id, name: goods.name, image: goods.main_image, specDesc, price, quantity }) } this.save() }, updateQuantity(key, delta) { const item this.items.find(item item.key key) if (!item) return item.quantity delta if (item.quantity 0) { this.items this.items.filter(item item.key ! key) } this.save() }, save() { uni.setStorageSync(cartItems, this.items) } } })这里有几个细节值得注意一是金额计算建议用分做整数运算避免浮点精度问题。前端JS里0.10.2不等于0.3金额计算时我会把价格乘以100转成整数计算完再除以100。二是价格以加入购物车那一刻的商品基础价格加规格加价为准因为下单时商品价格可能已经变了。如果要做严谨的版本控制应该在后端下单接口里重新校验价格前端购物车显示的价格只是一个预估值。购物车的UI是一个底部悬浮条左侧显示购物车图标和总数量右侧显示总价和“去结算”按钮点击弹出整个购物车列表。列表里每个条目可以加减数量、删除这几个交互都调用上面store里的action即可。3.3 下单与微信支付请求支付、支付回调一个都不能少确认订单页要做的事情很明确选择自取还是外送选择门店填备注展示购物车商品清单和合计金额最后点“提交订单”。提交订单的前端逻辑是这样的先把购物车items、pickupType、storeId、remark组装成订单数据调用后端创建订单接口。后端创建订单时需要做两件事一是根据订单明细重新计算总价不能信任前端传的价格二是在数据库生成订单记录状态置为待支付。创建成功后返回订单ID和订单号前端再发起微信支付。微信支付的调起方式比较简单uni.requestPayment是UniApp封装的统一API里面填微信支付需要的参数// 调起微信支付 const payRes await request({ url: /order/pay, method: POST, data: { orderNo } }) uni.requestPayment({ provider: wxpay, timeStamp: payRes.timeStamp, nonceStr: payRes.nonceStr, package: payRes.package, signType: MD5, paySign: payRes.paySign, success: () { // 支付成功跳转到订单详情 uni.redirectTo({ url: /pages/order/detail?orderNo${orderNo} }) }, fail: (err) { // 用户取消支付或支付失败 uni.showToast({ title: 支付未完成, icon: none }) } })注意paySign这些参数必须由后端生成。后端的逻辑是接收前端传的orderNo → 查数据库确认订单存在且未支付 → 调用微信支付统一下单API → 拿到prepay_id → 用prepay_id和商户密钥生成paySign → 返回给前端。前端绝对不能自己生成签名签名密钥必须保存在后端服务器。另外一个特别重要的点是前端uni.requestPayment的success回调不代表支付一定成功最终必须以微信支付的后端回调结果为准。后端要提供一个支付回调接口微信服务器在用户支付成功后会把结果POST到这个接口后端收到回调后更新订单状态为已支付。所以前端success回调里不要急着把订单状态改为已支付而是跳转到订单详情页让订单详情页通过轮询或订阅消息来获取最新状态。支付完成后还要处理一个常见的边界用户点了支付但中途取消订单仍然是待支付状态。这个可以再提供一个“继续支付”入口用户从订单详情页可以再次发起支付。另外为了避免残留大量未支付订单可以设置一个定时任务超过30分钟未支付就自动取消订单。3.4 订单状态机待支付、制作中、待取餐、已完成流转要理清订单状态是整个系统的业务核心。我设计的状态流转是待支付(0) → 已支付/制作中(1) → 待取餐(2) → 已完成(4)中间可以插入已取消(5)和退款(2)。如果是外送待取餐会变成配送中(3)再变成已完成。用户端的订单列表根据这些状态展示不同的操作按钮待支付显示“去支付”“取消订单”制作中显示“等待制作”待取餐显示“取餐码”和“呼叫店员”已完成只显示“再来一单”。门店端操作台看到新订单后点击“接单”把状态从已支付变成制作中出杯后点击“出餐”把状态变成待取餐或配送中。这里需要设计一个后端接口来更新订单状态同时带上状态变更的时间点。我的订单表里设计了pay_time、finish_time等字段方便后续统计制作时长、配送时长。还有一个实用的小功能取餐码。用户到店自取时订单详情页会展示一个取餐码一般是3位数门店端根据取餐码叫号。这个取餐码可以在用户支付成功时由后端生成按门店维度自增并每天重置。虽然实现不复杂但它是点餐系统体验感的关键细节加上它整个项目会完整很多。4. 配置、编译与上线HBuilderX打包微信小程序全流程4.1 manifest.json与小程序后台配置AppID、域名、白名单一次配好项目代码写完后最折腾人的其实是配置文件。用HBuilderX开发UniApp第一步是创建项目时配置自己的小程序AppID。如果你只是开发调试可以用测试号但要完整体验微信支付和上线发布就必须注册小程序账号并拿到正式AppID。在HBuilderX的manifest.json里需要找到“微信小程序配置”节点填写微信小程序的AppID。同时在“基础配置”里还需要设置uni-app的应用名称、版本号等。微信公众平台的配置是另一个重点登录小程序管理后台在“开发管理→开发设置”里配置服务器域名。request合法域名必须是HTTPS地址且域名需要ICP备案不能带端口号这个域名是前端所有请求的baseURL。小程序后台和开发者工具的关系是开发者工具里如果勾选了“不校验合法域名”请求本地或测试环境会方便但真机预览和上线版本必然要配好合法域名否则所有请求都会报“url not in domain list”。另一个容易被忽略的配置是业务域名用于web-view组件跳转和uploadFile合法域名如果做图片上传。对点餐项目来说商品图片如果是远程URL只需要在downloadFile合法域名里配置图片所在域名。4.2 顶部导航栏高度与安全区适配最容易翻车的位置小程序页面顶部导航栏的适配是我见过新手翻车最多的地方。默认情况下UniApp使用微信原生导航栏导航栏高度在不同手机上不一样尤其在iPhone的刘海屏和灵动岛机型上差异很大。如果你使用自定义导航栏就一定要处理状态栏高度和胶囊按钮位置。获取状态栏高度用uni.getSystemInfoSync().statusBarHeight获取胶囊按钮位置用uni.getMenuButtonBoundingClientRect()把两者结合起来就能算出导航栏的自定义高度const systemInfo uni.getSystemInfoSync() const menuRect uni.getMenuButtonBoundingClientRect() const navBarHeight (menuRect.top - systemInfo.statusBarHeight) * 2 menuRect.height这个公式的原理是胶囊按钮到状态栏底部的距离加上胶囊按钮本身的高度再乘以合适的比例就能得到自定义导航栏的高度。实现上我封装了一个navbar组件把状态栏高度和导航栏高度都计算好页面里直接引用就行。自定义导航栏时pages.json里对应页面的navigationStyle要配置为custom否则会看到两个导航栏叠加。另外别忘了底部安全区iPhone X等机型底部有home indicator自取/外送结算按钮容易被遮挡。处理方案是在底部按钮容器加上safe-area-inset-bottom样式.settle-bar { padding-bottom: constant(safe-area-inset-bottom); padding-bottom: env(safe-area-inset-bottom); }4.3 编译运行与常见问题排查实录开发调试阶段最常见的几个问题我直接整理成表格问题现象原因分析解决办法页面白屏pages.json里页面路径写错或组件编译错误检查pages数组路径运行到浏览器看控制台报错图片加载不出来图片域名不在downloadFile合法域名里或用了localhost开发时勾选不校验合法域名上线前配置域名按钮点击没反应事件没绑定或子组件遮挡检查click写法排查元素层级加z-index点击tabBar页面切换底部闪烁页面created重复请求接口请求加缓存或loading不要每次切换都拉全量数据微信支付调起报错未申请微信支付商户号或参数签名错误确认商户号已关联小程序后端签名逻辑排查真机预览请求失败开发者工具不校验域名真机默认校验小程序后台配置合法域名或用预览模式勾选“开发调试”加了规格后购物车数量乱specKey不唯一不同规格合并了用商品IDspecDesc组合作为key这里面的“点击tabBar页面切换底部闪烁”很多新手会忽略。原因是订单列表页每次进入都重新请求数据请求期间页面在渲染loading状态视觉上就会出现闪烁。解决办法很简单页面onShow时判断数据是否过期如果数据是最近10秒内的就直接用缓存不加loading这样切Tab就很顺滑。4.4 发布上线完整流程从HBuilderX到微信审核上线流程我走了一遍整个链路不复杂但步骤多每一步都不能漏第一步在HBuilderX菜单栏找到“发行→原生App-云打包”这里选“小程序-微信”点击发行后HBuilderX会自动编译并生成微信小程序代码输出到项目目录下的unpackage/dist/dev/mp-weixin或build/mp-weixin。第二步用微信开发者工具打开这个编译输出目录可以看到整个小程序项目。此时需要确认AppID正确、编译通过然后在开发者工具里进行真机调试。第三步确认功能没问题后点击开发者工具右上角“上传”按钮填入版本号和备注把代码上传到微信公众平台。此时可以在公众平台“版本管理”里看到这个开发版本先设为体验版让测试人员用真实手机扫码测试特别是支付流程一定要真机测。第四步在公众平台点击“提交审核”填写类目餐饮服务-点餐平台和功能页面审核一般一两天内出结果。审核通过后点击“发布”小程序就正式上线了。这里有个经验之谈发布前一定要把商家管理端的门店信息、商品信息都准备好因为新用户一进来看到空的菜单会直接流失。另外首次提交审核时如果涉及在线支付建议在提交审核页面附上测试账号和演示视频能加快审核速度。5. 一些个人经验与扩展方向整个奶茶店点餐系统从设计到上线我最强烈的体会有两点第一规格模型一定要先设计好它是奶茶点餐和普通商品售卖的本质区别设计不好后面购物车和订单全跟着遭殃。第二支付状态不能只看前端回调后端回调才是最终依据UI层可以给用户即时反馈但业务数据必须以后端为准。如果这个项目你做完还想继续扩展可以从这几个方向入手加一个简单的店长后台管理页面用H5做一个Web管理端统计每日营业额和热门商品给订单增加订阅消息提醒用户下单后微信服务通知会推送订单状态变化把配送距离和起送价做进去系统自动根据用户定位判断能否配送或者再接一个打印机门店端接单后自动打印小票。这些扩展做好后这个项目的完整度和商业价值会明显提升。还有一个小技巧分享开发期间商品图片不要用远程大图容易拖慢页面渲染速度本地测试时会感觉卡顿。建议先压缩图片到合理尺寸宽度750px左右、体积50KB以内必要时开启UniApp的图片懒加载首页菜单列表的滚动流畅度会好很多。祝你也能顺利把这个系统跑起来。
返回列表