7 天搞定 uni-app 电商小程序:首页轮播 + 商品列表 + 购物车 + 订单提交(附源码)
本教程基于Uniapp+Vue3+Vite+Uview+Pinia技术栈,7天实现电商小程序核心功能。内容涵盖:1.项目架构设计,包括技术栈选型、环境搭建和目录结构;2.核心功能开发,实现首页轮播、商品列表、购物车管理和订单提交;3.接口联调实战,封装网络请求处理跨域和错误;4.多端适配方案,通过条件编译和rpx单位确保兼容性;5.完整源码结构与部署指南。教程提供可直接复用的代码,帮助开发者快速掌
前言
本教程采用 Uniapp + Vue3 + Vite + Uview + Pinia 技术栈,7 天实现电商核心功能,同步讲解项目架构设计、接口联调技巧与多端适配方案,源码可直接复用。
第一章 项目架构设计(贯穿 Day1-Day2)
1.1 技术栈选型与环境搭建(Day1 上午)
核心技术栈
|
技术 |
作用 |
优势 |
|
Uni-app |
跨端框架 |
一次编写多端运行(微信 / 支付宝小程序、H5 等) |
|
Vue3 + Vite |
前端框架与构建工具 |
Composition API 更灵活,Vite 编译速度快 |
|
Uview |
UI 组件库 |
适配多端的高颜值组件 |
|
Pinia |
状态管理 |
替代 Vuex,支持 Vue3,适合购物车等数据管理 |
|
Uni-request |
网络请求 |
多端兼容的请求 API |
环境搭建步骤
1、安装 HBuilderX(3.90+)与 Node.js(16+)
2、新建项目:HBuilderX → 文件 → 新建 → 项目 → 选择 “uni-app 项目”,模板选 “默认模板(Vue3/Vite)”
3、安装依赖:在项目根目录执行
npm install uview-plus pinia --save
4、配置 Uview:在main.js中引入
import { createSSRApp } from 'vue'
import App from './App.vue'
import uviewPlus from 'uview-plus'
export function createApp() {
const app = createSSRApp(App)
app.use(uviewPlus)
return { app }
}
1.2 项目目录架构(Day1 下午)
ecommerce-mini/
├─ pages/ # 页面目录
│ ├─ index/ # 首页(轮播+商品列表)
│ ├─ cart/ # 购物车页面
│ └─ order/ # 订单页面(结算+提交)
├─ components/ # 公共组件
│ ├─ goods-card.vue # 商品卡片组件
│ └─ address-picker.vue # 地址选择组件
├─ store/ # Pinia状态管理
│ ├─ index.js # 状态管理入口
│ └─ cart.js # 购物车状态
├─ utils/ # 工具函数
│ ├─ request.js # 请求封装
│ └─ format.js # 数据格式化
├─ static/ # 静态资源(轮播图等)
└─ manifest.json # 多端配置文件
关键目录说明
- store/cart.js:用 Pinia 管理购物车数据,支持持久化存储
- utils/request.js:统一封装网络请求,处理拦截与适配
- components/:抽取复用组件,减少代码冗余
第二章 核心功能实现(Day2-Day5)
2.1 首页轮播实现(Day2)
2.1.1 基础结构(pages/index/index.vue)
利用 uni-app 原生swiper组件,支持自动轮播与指示器:
<template>
<view class="index-page">
<!-- 轮播图 -->
<swiper class="swiper"
indicator-dots="true"
indicator-color="#f8f8f8"
indicator-active-color="#007aff"
duration="3000"
autoplay="true">
<swiper-item v-for="(item, index) in bannerList" :key="index">
<image :src="item.imgUrl" class="swiper-img" mode="widthFix"></image>
</swiper-item>
</swiper>
<!-- 商品列表容器 -->
<view class="goods-list" v-if="goodsList.length">
<goods-card v-for="goods in goodsList" :key="goods.id" :goods="goods"></goods-card>
</view>
</view>
</template>
<style scoped>
.swiper { width: 100%; height: 200rpx; }
.swiper-img { width: 100%; height: 100%; }
.goods-list { padding: 20rpx; }
</style>
2.1.2 动态数据加载
在onLoad生命周期中请求轮播数据:
import { ref, onLoad } from 'vue'
import { getBannerList } from '@/api/home.js'
const bannerList = ref([])
onLoad(async () => {
// 调用接口获取轮播数据
const res = await getBannerList()
if (res.code === 200) {
bannerList.value = res.data
}
})
2.2 商品列表实现(Day3)
2.2.1 分页加载逻辑
<template>
<view class="goods-list">
<goods-card v-for="goods in goodsList" :key="goods.id" :goods="goods" @addCart="addCart"></goods-card>
<!-- 加载中提示 -->
<u-loading v-if="loading" mode="circle"></u-loading>
<!-- 无更多数据 -->
<view class="no-more" v-if="noMore">没有更多商品啦</view>
</view>
</template>
<script setup>
import { ref, onLoad, onReachBottom } from 'vue'
import { getGoodsList } from '@/api/goods.js'
import { useCartStore } from '@/store/cart.js'
const cartStore = useCartStore()
const goodsList = ref([])
const page = ref(1)
const pageSize = ref(10)
const loading = ref(false)
const noMore = ref(false)
// 加载商品列表
const loadGoods = async () => {
loading.value = true
const res = await getGoodsList({ page: page.value, pageSize: pageSize.value })
loading.value = false
if (res.code === 200) {
if (page.value === 1) {
goodsList.value = res.data.list
} else {
goodsList.value = [...goodsList.value, ...res.data.list]
}
// 判断是否有更多数据
noMore.value = res.data.list.length < pageSize.value
}
}
// 页面加载时初始化
onLoad(() => {
loadGoods()
})
// 下拉触底加载下一页
onReachBottom(() => {
if (!loading.value && !noMore.value) {
page.value++
loadGoods()
}
})
// 添加到购物车
const addCart = (goods) => {
cartStore.addItem({
id: goods.id,
name: goods.name,
price: goods.price,
imgUrl: goods.imgUrl,
num: 1
})
}
</script>
2.2.2 商品卡片组件(components/goods-card.vue)
<template>
<view class="goods-card" @click="toDetail">
<image :src="goods.imgUrl" class="goods-img"></image>
<view class="goods-info">
<view class="goods-name">{{ goods.name }}</view>
<view class="goods-price">¥{{ goods.price.toFixed(2) }}</view>
<u-button class="add-btn" size="mini" @click.stop="handleAdd">加入购物车</u-button>
</view>
</view>
</template>
<script setup>
const props = defineProps({
goods: {
type: Object,
required: true
}
})
const emit = defineEmits(['addCart'])
const handleAdd = () => {
emit('addCart', props.goods)
}
const toDetail = () => {
uni.navigateTo({ url: `/pages/goods/detail?id=${props.goods.id}` })
}
</script>
2.3 购物车功能实现(Day4)
2.3.1 Pinia 状态管理(store/cart.js)
import { defineStore } from 'pinia'
import { reactive, computed } from 'vue'
export const useCartStore = defineStore('cart', () => {
// 购物车数据(本地缓存初始化)
const state = reactive({
cartItems: uni.getStorageSync('cartItems') || [],
allChose: false
})
// 计算已选中商品总数
const selectedCount = computed(() => {
return state.cartItems.reduce((total, item) => {
return item.isChoose ? total + item.num : total
}, 0)
})
// 计算已选中商品总价
const selectedPrice = computed(() => {
return state.cartItems.reduce((total, item) => {
return item.isChoose ? total + (item.price * item.num) : total
}, 0).toFixed(2)
})
// 添加商品到购物车
const addItem = (goods) => {
const existingItem = state.cartItems.find(item => item.id === goods.id)
if (existingItem) {
existingItem.num += goods.num
} else {
state.cartItems.push({ ...goods, isChoose: true })
}
saveToLocal()
}
// 修改商品数量
const changeNum = (id, num) => {
const item = state.cartItems.find(item => item.id === id)
if (item) item.num = num
saveToLocal()
}
// 切换商品选中状态
const toggleChoose = (id) => {
const item = state.cartItems.find(item => item.id === id)
if (item) item.isChoose = !item.isChoose
updateAllChose()
saveToLocal()
}
// 更新全选状态
const updateAllChose = () => {
state.allChose = state.cartItems.length > 0 && state.cartItems.every(item => item.isChoose)
}
// 全选/取消全选
const toggleAllChose = () => {
state.allChose = !state.allChose
state.cartItems.forEach(item => item.isChoose = state.allChose)
saveToLocal()
}
// 本地缓存购物车数据
const saveToLocal = () => {
uni.setStorageSync('cartItems', state.cartItems)
}
return {
state,
selectedCount,
selectedPrice,
addItem,
changeNum,
toggleChoose,
toggleAllChose
}
})
2.3.2 购物车页面(pages/cart/index.vue)
<template>
<view class="cart-page">
<u-empty v-if="cartStore.state.cartItems.length === 0" mode="cart"></u-empty>
<view class="cart-list" v-else>
<view class="cart-item" v-for="item in cartStore.state.cartItems" :key="item.id">
<u-checkbox :checked="item.isChoose" @change="() => cartStore.toggleChoose(item.id)"></u-checkbox>
<image :src="item.imgUrl" class="item-img"></image>
<view class="item-info">
<view class="item-name">{{ item.name }}</view>
<view class="item-price">¥{{ item.price.toFixed(2) }}</view>
</view>
<u-number-box v-model="item.num" @change="(val) => cartStore.changeNum(item.id, val)" min="1"></u-number-box>
</view>
</view>
<!-- 底部结算栏 -->
<view class="cart-footer" v-if="cartStore.state.cartItems.length > 0">
<u-checkbox :checked="cartStore.state.allChose" @change="cartStore.toggleAllChose">全选</u-checkbox>
<view class="total-info">
<view>合计:¥{{ cartStore.selectedPrice }}</view>
<view>共{{ cartStore.selectedCount }}件商品</view>
</view>
<u-button class="settle-btn" @click="toSettle">去结算</u-button>
</view>
</view>
</template>
<script setup>
import { useCartStore } from '@/store/cart.js'
const cartStore = useCartStore()
const toSettle = () => {
// 获取选中的商品
const selectedItems = cartStore.state.cartItems.filter(item => item.isChoose)
if (selectedItems.length === 0) {
uni.showToast({ title: '请选择商品', icon: 'none' })
return
}
// 跳转结算页,携带选中商品
uni.navigateTo({
url: `/pages/order/settle?selectedItems=${encodeURIComponent(JSON.stringify(selectedItems))}`
})
}
</script>
2.4 订单提交实现(Day5)
2.4.1 结算页面(pages/order/settle.vue)
<template>
<view class="settle-page">
<!-- 地址选择 -->
<view class="address-card" @click="chooseAddress">
<u-icon name="location" size="36" color="#666"></u-icon>
<view class="address-info" v-if="selectedAddress">
<view class="receiver">{{ selectedAddress.receiver }} {{ selectedAddress.phone }}</view>
<view class="address">{{ selectedAddress.fullAddress }}</view>
</view>
<view class="no-address" v-else>请选择收货地址</view>
<u-icon name="arrow-right" size="28" color="#999"></u-icon>
</view>
<!-- 选中商品列表 -->
<view class="goods-list">
<view class="goods-item" v-for="item in selectedItems" :key="item.id">
<image :src="item.imgUrl" class="item-img"></image>
<view class="item-info">
<view class="item-name">{{ item.name }}</view>
<view class="item-price">¥{{ item.price.toFixed(2) }} x {{ item.num }}</view>
</view>
</view>
</view>
<!-- 底部提交栏 -->
<view class="settle-footer">
<view class="total">总计:¥{{ totalPrice.toFixed(2) }}</view>
<u-button class="submit-btn" @click="submitOrder" :loading="submitting">提交订单</u-button>
</view>
</template>
<script setup>
import { ref, onLoad, computed } from 'vue'
import { submitOrder } from '@/api/order.js'
// 接收购物车选中的商品
const selectedItems = ref([])
const selectedAddress = ref(null)
const submitting = ref(false)
// 计算订单总价
const totalPrice = computed(() => {
return selectedItems.value.reduce((total, item) => {
return total + (item.price * item.num)
}, 0)
})
onLoad((options) => {
// 解析传入的选中商品
selectedItems.value = JSON.parse(decodeURIComponent(options.selectedItems))
// 加载默认地址
loadDefaultAddress()
})
// 加载默认地址
const loadDefaultAddress = async () => {
const res = await getDefaultAddress()
if (res.code === 200) {
selectedAddress.value = res.data
}
}
// 选择地址(跳转地址选择页)
const chooseAddress = () => {
uni.navigateTo({ url: '/pages/address/list' })
}
// 提交订单
const submitOrder = async () => {
if (!selectedAddress.value) {
uni.showToast({ title: '请选择收货地址', icon: 'none' })
return
}
submitting.value = true
try {
const res = await submitOrder({
addressId: selectedAddress.value.id,
totalAmount: totalPrice.value,
orderItems: selectedItems.value.map(item => ({
goodsId: item.id,
quantity: item.num,
price: item.price
}))
})
if (res.code === 200) {
uni.showToast({ title: '订单提交成功' })
// 跳转支付页面
uni.navigateTo({ url: `/pages/order/pay?orderNo=${res.data.orderNo}` })
}
} catch (err) {
uni.showToast({ title: '订单提交失败', icon: 'none' })
} finally {
submitting.value = false
}
}
</script>
第三章 接口联调实战(Day6)
3.1 请求封装(utils/request.js)
统一处理请求拦截、响应拦截与多端适配:
import { baseURL } from '@/config/index.js'
// 创建请求实例
const request = (options = {}) => {
// 设置默认请求头
options.header = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${uni.getStorageSync('token') || ''}`,
...options.header
}
// 条件编译:H5端使用fetch优化
// #ifdef H5
if (window.fetch && !options.forceUniRequest) {
return new Promise((resolve, reject) => {
fetch(`${baseURL}${options.url}`, {
method: options.method || 'GET',
headers: options.header,
body: options.data ? JSON.stringify(options.data) : null
})
.then(response => response.json())
.then(data => {
if (data.code === 401) {
// 登录失效处理
uni.navigateTo({ url: '/pages/login/index' })
reject(data)
}
resolve(data)
})
.catch(err => reject(err))
})
}
// #endif
// 其他端使用uni.request
return new Promise((resolve, reject) => {
uni.request({
url: `${baseURL}${options.url}`,
method: options.method || 'GET',
data: options.data,
header: options.header,
timeout: 10000,
success: (res) => {
// 状态码非200拦截
if (res.statusCode !== 200) {
uni.showToast({ title: `请求失败:${res.statusCode}`, icon: 'none' })
reject(res)
return
}
// 业务码401(登录失效)处理
if (res.data.code === 401) {
uni.navigateTo({ url: '/pages/login/index' })
reject(res.data)
return
}
resolve(res.data)
},
fail: (err) => {
uni.showToast({ title: '网络请求失败', icon: 'none' })
reject(err)
}
})
})
}
// 封装GET请求
export const get = (url, params = {}, options = {}) => {
return request({ url, method: 'GET', data: params, ...options })
}
// 封装POST请求
export const post = (url, data = {}, options = {}) => {
return request({ url, method: 'POST', data, ...options })
}
export default request
3.2 业务 API 封装(api/order.js)
import { post, get } from '@/utils/request.js'
// 获取默认地址
export const getDefaultAddress = () => {
return get('/address/default')
}
// 提交订单
export const submitOrder = (data) => {
return post('/order/submit', data)
}
// 获取订单详情
export const getOrderDetail = (orderNo) => {
return get(`/order/detail?orderNo=${orderNo}`)
}
3.3 接口联调注意事项
1、跨域处理:后端配置 CORS(Access-Control-Allow-Origin: *),小程序端需配置域名白名单
2、并发控制:微信小程序限制 10 个并发请求,高频场景用防抖(如搜索)
3、错误处理:统一捕获 401(登录失效)、500(服务器错误)等状态码
4、Mock 数据:开发初期用 Mock.js 模拟接口,示例:
// 模拟轮播图接口
Mock.mock('/api/banner/list', 'get', {
code: 200,
data: [
{ id: 1, imgUrl: '/static/banner1.jpg' },
{ id: 2, imgUrl: '/static/banner2.jpg' }
]
})
第四章 多端适配方案(Day7)
4.1 样式适配
1、单位统一:全部使用 rpx(750rpx = 屏幕宽度),避免 px
2、Flex 布局:替代浮动布局,适配不同屏幕
3、条件编译样式:
/* 微信小程序特有样式 */
/* #ifdef MP-WEIXIN */
.nav-bar { padding-top: 20rpx; }
/* #endif */
/* H5特有样式 */
/* #ifdef H5 */
.nav-bar { padding-top: 40rpx; }
/* #endif */
4.2 组件与 API 适配
4.2.1 组件适配
- 使用 Uview 组件库(天然适配多端),避免原生平台特有组件
- 自定义组件添加多端兼容判断:
<!-- 支付宝小程序使用button,其他端使用u-button -->
<!-- #ifdef MP-ALIPAY -->
<button class="alipay-btn">支付</button>
<!-- #endif -->
<!-- #ifndef MP-ALIPAY -->
<u-button class="normal-btn">支付</u-button>
<!-- #endif -->
4.2.2 API 适配
- 网络请求:H5 端优先用 fetch,其他端用 uni.request
- 存储 API:统一用 uni.setStorageSync/uni.getStorageSync,替代平台特有 API
- 支付 API:条件编译处理不同平台支付:
// 发起支付
const pay = async (orderNo) => {
const res = await getPayParams(orderNo)
// 微信小程序支付
// #ifdef MP-WEIXIN
uni.requestPayment({
timeStamp: res.data.timeStamp,
nonceStr: res.data.nonceStr,
package: res.data.package,
signType: 'MD5',
paySign: res.data.paySign,
success: () => uni.showToast({ title: '支付成功' })
})
// #endif
// 支付宝小程序支付
// #ifdef MP-ALIPAY
my.tradePay({
tradeNO: res.data.tradeNo,
success: () => uni.showToast({ title: '支付成功' })
})
// #endif
}
4.3 多端测试清单
|
平台 |
测试重点 |
工具 |
|
微信小程序 |
域名白名单、支付功能、分包加载 |
微信开发者工具 |
|
支付宝小程序 |
组件兼容性、支付参数 |
支付宝开发者工具 |
|
H5 |
跨域、路由模式、响应式布局 |
浏览器 DevTools |
第五章 源码与部署
5.1 完整源码结构
ecommerce-mini/
├─ pages/ # 页面(首页/购物车/订单)
├─ components/ # 公共组件(商品卡片/地址选择)
├─ store/ # Pinia状态管理(cart.js核心)
├─ utils/ # 工具函数(request.js/format.js)
├─ api/ # 接口封装(home/goods/order.js)
├─ static/ # 静态资源(banner1.jpg等)
├─ config/ # 配置文件(baseURL等)
├─ manifest.json # 多端配置
└─ package.json # 依赖配置
5.2 部署步骤
1、微信小程序:
- 在 HBuilderX 中点击 “发行”→“微信小程序”
- 用微信开发者工具打开 dist/dev/mp-weixin 目录
- 上传代码至微信公众平台审核
2、H5:发行→H5→选择部署路径→上传至服务器
5.3 注意事项
- 小程序端需在开发者平台配置合法域名
- 支付功能需申请对应平台的支付权限
- 上线前执行多端兼容性测试
结语
通过 7 天的学习,你已掌握 uni-app 电商小程序的核心开发能力:从架构设计到功能实现,从接口联调到多端适配。实际开发中可基于本教程扩展更多功能(如商品搜索、个人中心),源码可根据需求调整优化。
更多推荐

所有评论(0)