简介:这是一份基于 uni-app 开发的微信商城小程序通用模板,适合前端入门者、小程序开发者以及需要快速搭建商城原型的学生或创业团队。模板覆盖商品展示、购物车、订单、个人中心等典型业务模块,预置常用图表、图标与实时通信支持,功能确认可运行,可直接在此基础上做二次开发。压缩包共490个文件,核心代码以275个js逻辑文件和117个vue页面组件为主,辅以json配置、scss/css样式以及wxml/wxss等小程序原生结构文件,整体仅2.04MB,目录轻量清晰,适合按模块拆解学习。目前已有382人学习浏览。借助该模板,读者既能理解 uni-app 跨端项目的组织方式、页面路由与状态管理思路,也可把界面和后端接口改造成独立商城应用;无论用于课程设计、毕业设计还是初期项目立项,都是实用且易上手的参考代码。
1. 开篇:为什么说商城模板的核心不在页面,而在几条数据流
市面上并不缺 UI 好看的商城小程序模板,但很多开发者下载后卡在第一步:HBuilderX 里跑起来白屏、真机预览请求全部失败、SKU 选不了、支付唤起没反应。这份基于 uniapp 开发的微信商城小程序通用模板,价值不在它长得好看,而在于登录、购物车、SKU、支付这几条链路是完整贯通的。它给出的是从页面到接口的一整套数据流,适合两类人:一是接单或自研小程序、想省掉基础搭建时间的前端开发者,二是团队内还没有统一商城代码规范、需要一个可复用骨架的负责人。无论走哪条路,你先要能把它跑起来,然后才谈得上改造。
2. 技术栈与目录结构:先搞清 uniapp 模板的骨架长什么样
2.1 技术选型:为什么用 uniapp 而不是原生微信小程序
原生小程序开发的问题在于:一套代码只能跑微信端,后续如果要出支付宝小程序或 App,又得重新写一遍。uniapp 的核心优势是编译时把 Vue 语法转成各端代码,模板本身用 Vue 2 语法编写,运行在小程序端时会被编译为 WXML 和 WXSS。对商城这类页面结构偏固定、交互以列表和表单为主的项目,用 uniapp 能大幅减少重复开发。
模板里没有引入重型 UI 库,商品卡片、SKU 弹层、数量选择器这些组件都是手写完成的,只是引用了 uniapp 内置组件如 view、text、image、scroll-view,以及 vuex 做购物车和用户状态的统一管理。网络请求部分基于 uni.request 封装了一层,方便统一注入 token、处理错误码。
2.2 目录结构:每个目录负责什么
拿到模板后,第一件事不是看代码,而是先把这个项目的目录结构摸清。模板核心结构如下:
├── App.vue # 应用入口文件,全局生命周期 ├── main.js # Vue 实例入口,注册 vuex ├── manifest.json # 应用配置:appid、小程序权限、SDK 配置 ├── pages.json # 页面路由与 tabBar 配置 ├── uni.scss # 全局 scss 变量,主题色修改入口 ├── api/ # 接口层,统一管理请求地址 │ ├── request.js # uni.request 封装 │ ├── goods.js # 商品相关接口 │ ├── cart.js # 购物车接口 │ └── order.js # 订单与支付接口 ├── store/ # vuex 状态管理 │ ├── index.js # store 主入口 │ ├── modules/ │ │ ├── cart.js # 购物车模块 │ │ ├── user.js # 用户信息与登录态 │ │ └── goods.js # 商品与 SKU 状态 ├── pages/ │ ├── index/ # 首页 │ ├── goods/ │ │ ├── list.vue # 商品列表页 │ │ └── detail.vue # 商品详情页 │ ├── cart/ # 购物车页 │ ├── order/ │ │ ├── confirm.vue # 确认订单页 │ │ └── pay-success.vue # 支付成功页 │ ├── user/ # 个人中心页 │ └── login/ # 登录页 ├── components/ │ ├── goods-card.vue # 商品卡片组件 │ ├── sku-popup.vue # SKU 选择弹层 │ └── number-box.vue # 数量选择器 └── static/ # 静态资源:图片、图标api、store、components、pages 四层分离,是这套模板后续二次开发最依赖的骨架。常见的翻车姿势是:开发者拿到模板后直接在页面里写 uni.request,跳过 api 层,导致后续换接口时所有页面都要改一遍。这个模板的 api 层就是用来规避这种问题的。
2.3 manifest.json 与 pages.json:跑起来之前必改的两处配置
很多人下载模板后直接运行,结果在微信开发者工具里报错或白屏,绝大多数问题出在 manifest.json。这个文件里要注意的重点包括:
- 微信小程序 appid:模板自带的是测试 appid,真机预览和上传必须替换成自己的
- 权限配置:如果模板里用到定位、获取手机号等功能,需要在 mp-weixin 节点下的 permission 里声明
- 基础库版本:最低版本设置太高,低版本微信会打不开;设置太低,部分 API 用不了
pages.json 决定页面路由和底部 tabBar。模板里底部 tab 有四个,对应首页、分类、购物车、个人中心,配置在 tabBar.list 数组里。新增页面时必须在 pages 数组里注册,顺序决定页面加载顺序,首个页面就是启动页。
{ "pages": [ { "path": "pages/index/index", "style": { "navigationBarTitleText": "商城首页" } }, { "path": "pages/goods/list", "style": { "navigationBarTitleText": "商品列表", "enablePullDownRefresh": true } } ], "tabBar": { "list": [ { "pagePath": "pages/index/index", "text": "首页" }, { "pagePath": "pages/cart/cart", "text": "购物车" } ] } }pages 数组里每个对象的 path 是页面路径,style 中的 navigationBarTitleText 控制导航栏标题,enablePullDownRefresh 决定是否支持下拉刷新。tabBar 的 pagePath 必须是 pages 里已注册的路径,否则运行时会提示找不到页面。改完这两处配置后,再用 HBuilderX 运行到微信开发者工具,基本就能看到首页了。
3. 核心模块拆解:商品从列表到下单的完整链路
3.1 首页信息流:request 封装与分页加载
商城首页最基础的能力是商品列表加载。这个模板里所有请求都走 api/request.js 这个封装,核心代码逻辑如下:
// api/request.js const BASE_URL = 'https://api.example.com/v1' export function request(options) { return new Promise((resolve, reject) => { uni.request({ url: BASE_URL + options.url, method: options.method || 'GET', data: options.data || {}, header: { 'Content-Type': 'application/json', 'Authorization': uni.getStorageSync('token') || '' }, success: (res) => { if (res.statusCode === 200 && res.data.code === 0) { resolve(res.data.data) } else if (res.statusCode === 401) { uni.navigateTo({ url: '/pages/login/login' }) reject(res) } else { uni.showToast({ title: res.data.msg || '请求失败', icon: 'none' }) reject(res) } }, fail: (err) => { uni.showToast({ title: '网络异常', icon: 'none' }) reject(err) } }) }) }封装的核心逻辑有三个:统一拼接 BASE_URL,调用方只传相对路径;从本地存储读取 token 并注入请求头,不用每个页面手写;对返回结构做了约定,code 为 0 时业务成功,401 时自动跳登录页,其他 code 统一 toast 错误信息。
首页商品列表调用商品接口时,用到 uniapp 的分页监听。模板中的做法是在 onReachBottom 生命周期里触发加载下一页:
// pages/index/index.vue export default { data() { return { goodsList: [], page: 1, pageSize: 10, loading: false, finished: false } }, onLoad() { this.loadGoods(true) }, onReachBottom() { if (!this.loading && !this.finished) { this.page += 1 this.loadGoods() } }, methods: { async loadGoods(reset = false) { if (this.loading) return this.loading = true try { const data = await getGoodsList({ page: this.page, pageSize: this.pageSize }) if (reset) { this.goodsList = data.list } else { this.goodsList = this.goodsList.concat(data.list) } this.finished = data.list.length < this.pageSize } finally { this.loading = false } } } }onReachBottom 是 uniapp 页面滚动到底部的生命周期,相当于小程序原生的 onReachBottom。每次触发时先判断 loading 和 finished,避免重复请求和到底后继续请求。reset 参数用于区分是首次加载还是分页追加,首次加载同时会把 page 重置为 1。
3.2 商品详情与 SKU:把规格选择做成可复用弹层
商品详情页的复杂度集中在 SKU 选择。模板中商品规格的数据结构是典型的多规格组合,比如一个商品有颜色和尺码两个维度:
{ "spec_list": [ { "id": 1, "name": "颜色", "values": ["黑色", "白色"] }, { "id": 2, "name": "尺码", "values": ["S", "M", "L"] } ], "sku_list": [ { "spec_ids": "1:黑色,2:S", "price": 199, "stock": 50 }, { "spec_ids": "1:黑色,2:M", "price": 199, "stock": 30 } ] }spec_list 描述规格维度,sku_list 描述具体可售组合。弹层组件的核心逻辑是:用户每选中一个规格值,就把该规格值加入已选集合,然后遍历 sku_list 判断当前组合是否可售。
<template> <view v-if="show" class="sku-mask"> <view class="sku-panel"> <view v-for="spec in specList" :key="spec.id" class="spec-group"> <text class="spec-name">{{ spec.name }}</text> <view class="spec-values"> <view v-for="value in spec.values" :key="value" :class="['spec-item', selectedSpec[spec.name] === value ? 'active' : '']" @tap="selectSpec(spec.name, value)" > {{ value }} </view> </view> </view> </view> </view> </template> <script> export default { data() { return { selectedSpec: {} } }, methods: { selectSpec(specName, value) { this.$set(this.selectedSpec, specName, value) const combo = Object.entries(this.selectedSpec) .map(([name, val]) => `${name}:${val}`) .join(',') const matched = this.skuList.find(sku => sku.spec_ids === combo) if (matched) { this.setState({ price: matched.price, stock: matched.stock }) } else { this.setState({ price: 0, stock: 0 }) } } } } </script>selectSpec 方法做的事是:更新当前选中规格集合,把已选规格拼成和 sku_list 中 spec_ids 一致的字符串,然后匹配可售组合。匹配不到就置空价格和库存,提示用户当前组合不可选。这段逻辑不依赖任何第三方库,是模板里值得反复看的片段,你在电商项目里做秒杀、预售、多规格商品时都能直接复用这套思路。
3.3 购物车与结算:vuex 管理全局状态
购物车数据在多个页面共享,模板用 vuex 的 cart 模块来管理。要点是购物车列表和选中状态存在 store 里,页面只负责渲染和派发 action。
// store/modules/cart.js export default { state: { cartList: [], checkedList: [] }, getters: { totalCount(state) { return state.cartList.reduce((sum, item) => sum + item.count, 0) }, totalPrice(state) { return state.cartList .filter(item => state.checkedList.includes(item.id)) .reduce((sum, item) => sum + item.price * item.count, 0) } }, mutations: { ADD_CART(state, goods) { const exists = state.cartList.find(item => item.id === goods.id) if (exists) { exists.count += goods.count } else { state.cartList.push({ ...goods, count: goods.count }) } }, TOGGLE_CHECK(state, id) { const idx = state.checkedList.indexOf(id) if (idx > -1) { state.checkedList.splice(idx, 1) } else { state.checkedList.push(id) } } } }totalPrice 这个 getter 依赖 checkedList,只有勾选了的商品才计入结算金额。这个设计比在购物车页面里用 data 临时计算更稳,因为从购物车进入确认订单页后,仍然需要读取同样的选中数据。结算金额的计算、勾选状态的同步,全部以 store 为准,页面销毁也不会丢。
确认订单页的流程相对固定:从 cart 模块读取 checkedList,调下单接口,拿到订单 id 后跳转支付。这里有个开发中容易忽略的点:从购物车到确认订单的跳转,需要把选中的商品数据通过页面参数或 store 传递,模板里是用 store 读取的,避免 URL 传参过长被截断。
4. 登录与支付:前后端联调里最绕的两个环节
4.1 微信登录态:uni.login 与自定义登录的衔接
模板的登录流程不是直接用微信的 openid 作为用户标识,而是走「微信授权码换 token」的标准流程。为什么需要这一层?因为商户后台需要把微信身份和自己系统的用户体系绑定,后续的订单、积分、会员等级都挂在自有用户 id 上。
// api/user.js export function wxLogin() { return new Promise((resolve, reject) => { uni.login({ provider: 'weixin', success: async (loginRes) => { try { const res = await request({ url: '/user/login', method: 'POST', data: { code: loginRes.code } }) uni.setStorageSync('token', res.token) uni.setStorageSync('userInfo', res.userInfo) resolve(res) } catch (err) { reject(err) } }, fail: (err) => { reject(err) } }) }) }uni.login 返回的 code 是一次性的,有效期大概几分钟,后端拿这个 code 调微信接口换 openid 和 session_key,再生成自己系统的 token 返回给前端。这个 token 就是后续所有请求头里 Authorization 的值,也是 request.js 封装里从 storage 读取的那个 token。
实际开发中要注意 code 的时效性。有的开发者把 code 存在本地复用,结果第二次请求时后端直接报 code 无效。正确做法是每次登录都重新 uni.login 获取新的 code。如果模板里登录后请求接口还是报 401,优先检查后端是否真的收到了有效 code。
4.2 支付流程:从下单到 uni.requestPayment
支付流程是商城项目里最容易出问题的环节。模板的做法是:确认订单页调下单接口,后端返回支付所需的参数,前端再调 uni.requestPayment 拉起微信支付面板。
// api/order.js export function createOrder(orderData) { return request({ url: '/order/create', method: 'POST', data: orderData }) } export function payOrder(orderId) { return request({ url: '/order/pay', method: 'POST', data: { orderId } }) }// pages/order/confirm.vue async function submitOrder() { const orderData = { goodsList: this.checkedGoods, addressId: this.addressId, remark: this.remark } const order = await createOrder(orderData) const payParams = await payOrder(order.orderId) uni.requestPayment({ provider: 'wxpay', timeStamp: payParams.timeStamp, nonceStr: payParams.nonceStr, package: payParams.package, signType: payParams.signType, paySign: payParams.paySign, success: () => { uni.redirectTo({ url: '/pages/order/pay-success' }) }, fail: (err) => { uni.showToast({ title: '支付已取消', icon: 'none' }) } }) }一定要注意:uni.requestPayment 里的 package 参数是后端返回的,不是自己拼的。很多新手在这里写死或写错,导致支付面板一直起不来。时间戳 timeStamp 是秒级字符串,后端返回什么就传什么,前端不需要转换格式,paySign 的签名规则完全由后端控制。
4.3 与后端联调时的接口约定
一个商城模板能否快速接上真实业务,就看 api 层和返回结构是否规范。模板里已经把返回结构统一为:
{ "code": 0, "msg": "success", "data": { "list": [], "total": 100 } }code 为 0 表示成功,非 0 表示业务错误,401 特殊处理为登录失效。如果你们的后端返回结构不是这样,改 request.js 里的判断逻辑就行。我一般会在模板基础上增加一个拦截 uni.showToast 的开关,调试阶段把错误信息显示全,上线后只显示统一文案,避免把后端异常直接暴露给用户。
5. 避坑指南:跑通这套模板时的 5 个高频问题
5.1 HBuilderX 运行后白屏或页面找不到
现象:模板在 HBuilderX 中编译成功,但微信开发者工具打开后白屏,或控制台提示 page 路径不存在。
原因:pages.json 的 pages 数组里注册了不存在的页面路径,或改过目录结构后路径没有同步更新。
解决:打开 pages.json,逐个核对 pages 数组里的 path 是否与 pages 目录下的真实文件对应;再确认 manifest.json 里的微信 appid 是否填写正确。还有一个常见原因:首次运行没重新编译,HBuilderX 里点运行后,如果改了 pages.json 需要重新编译,增量编译有时不会刷新路由。
5.2 真机预览时请求全部失败
现象:开发者工具里接口正常,真机预览后所有请求都是 fail 或报域名不在合法列表。
原因:微信小程序要求所有请求域名必须配置在 mp 后台的 request 合法域名里,且必须是 HTTPS。开发时工具可以勾选「不校验合法域名」,真机上这个开关不生效。
解决:开发阶段在微信开发者工具的「详情-本地设置」里勾选不校验合法域名;真机调试或上线前,把接口域名加到 mp 后台的 request 合法域名列表。域名必须是备案过的 HTTPS,且不能带端口。如果只是临时联调,可以用真机调试模式绕过,但最终上线前必须配好域名。
5.3 支付唤起失败,报 package 参数错误
现象:调 uni.requestPayment 时提示 package 参数错误,或支付面板弹出来立即消失。
原因:package 参数是从后端获取的,前端把它写错或漏传;也可能是后端生成签名时用的 appid 与小程序 appid 不一致。
解决:确认 uni.requestPayment 的入参中 timeStamp、nonceStr、package、signType、paySign 全部由后端下单接口返回,前端严格透传。检查后端支付证书里的商户号 mch_id 和 appid 对应关系,支付签名用 v2 还是 v3 的规则也要和后端确认清楚。
5.4 商品图片裂开或不显示
现象:列表页图片加载不出来,或本地调试正常、部署后图片失效。
原因:图片如果走的是 http 协议,在小程序里会被拦截;部分图片服务器做了防盗链,拒绝了小程序的请求头。
解决:图片地址必须保证可公网访问的 HTTPS 链接,且响应头中不能包含禁止外部引用的 X-Frame-Options 或防盗链校验。开发时如果图片来自本地 static 目录,确认路径是相对路径而不是绝对路径。真机预览后图片挂掉的,多半是图片域名没有在 mp 后台的 downloadFile 合法域名里配置。
5.5 SKU 选中后价格库存不刷新
现象:在 SKU 弹层里点击不同规格,价格和库存没有变化,甚至显示为 0。
原因:SKU 弹层里 spec_ids 的拼接格式与 sku_list 里的 spec_ids 字段不一致,比如规范里用的是冒号分隔,后端返回的是下划线分隔,导致匹配失败。
解决:先打印出前端拼接的字符串和后端返回的 spec_ids,一行一行对比。模板里约定的是规格名:规格值,规格名:规格值的格式,如果后端格式不同,统一在后端转换,不要在前端做兼容处理,否则每种商品都可能有不同的拼接规则,维护成本极高。
6. 二次开发实战:把通用模板改成你自己的线上项目
6.1 换主题:改 uni.scss 一处,全局变色
模板里的主色、辅助色、圆角、间距全部定义在 uni.scss 中。改动最集中的是 $primary-color 这个变量,它是底部 tab 选中态、价格文字、按钮背景、SKU 选中态共用的颜色。这个模板把颜色变量抽出来做了全局覆盖,改一处全局生效,省去每个页面逐个找颜色的麻烦。如果你要改成多主题,则可以再抽出 theme.scss,配合条件编译做不同端的差异化。
6.2 新增页面:走一遍完整的四步注册
加一个新页面不是只建 .vue 文件,还要完成四个动作:在 pages 目录下创建 vue 文件;在 pages.json 的 pages 数组里注册路由;如果要在底部 tab 显示,把路径加进 tabBar.list;如果页面是商品详情的二级入口,在组件里写好跳转。这套流程不复杂,但容易漏。我自己的习惯是每次新增页面都强制走一遍「建文件、注册路由、起本地预览、真机看一次」的完整流程,确认页面可以正常编译后再写业务逻辑,避免攒了一堆页面最后一起编译报错,排查起来非常痛苦。
6.3 接真实后端:只改 api 层,不碰页面
模板的价值在于 api 层与页面层是解耦的。替换成真实后端时,只需要改 request.js 里的 BASE_URL 和每个 api 文件里的 url、data 字段名。如果后端返回结构和模板不一致,优先在 request.js 或 api 层做一个适配层,把后端的返回字段映射成模板页面里能识别的结构。不要去页面里逐行改字段名,那样改完这个项目,下一个项目还是会踩同样的坑。
这套模板我从头到尾拆过一遍,最深的感触是:下载一个商城模板,先看 pages.json,再看 store 里的模块划分,最后才看页面长什么样。从那以后,我每次接手任何 uniapp 项目,都强制自己先走一遍「看路由、看状态、看请求封装」的三步流程,确认这三点没问题之前,不写任何业务代码。希望这个思路对你也有用。
本文还有配套的精品资源,点击获取