简介:这是一套基于Vue.js开发的轻量级手机商城活动页模板,面向前端初学者与中小型电商项目开发者,旨在快速搭建响应式移动端商品展示、分类导航、购物车及用户交互功能。资源包体积仅238KB,结构精简,包含HTML页面骨架、CSS样式文件、Vue组件(如商品卡片、导航栏)、JavaScript业务逻辑及基础API调用示例,整体聚焦于核心交互实现而非全栈功能,适合用于学习Vue组件化开发、响应式布局实践与SPA页面跳转机制。目前已有2574人学习下载,读者可直接运行查看效果,快速掌握商城类页面的模块拆分逻辑、数据绑定方式与移动端适配技巧,并基于现有结构灵活扩展商品详情、登录态管理等模块。
1. Vue 商城模板:不是套个UI就叫“能跑”,它得扛住真实用户加购、结算、订单状态流转的完整链路
很多同学拿到一个“Vue 商城模板”,第一反应是 npm install、npm run serve,看到首页轮播图和商品列表就以为“毕设/课设稳了”。结果一加购物车就报Cannot read property 'push' of undefined,一提交订单就跳转 404,后台接口连不上还死活找不到 mock 数据在哪配——这不是模板的问题,是模板没告诉你:它默认只交付「界面骨架」,而你真正要交的,是一套能走通「浏览→加购→下单→支付模拟→订单查询」闭环的最小可行系统(MVP)。这份 Vue 商城模板,专为需要快速交付完整流程、又不想从零搭路由/状态管理/请求拦截的同学设计:它已预置 Vuex 模块化状态(cart、user、order)、Axios 请求封装(含 loading 自动开关、错误统一拦截)、Mock.js 模拟后端(支持登录态、库存扣减、订单生成),甚至预留了微信支付 H5 调用占位符。适合课程设计需演示全流程、毕设需体现工程规范、或实习前想练手真实电商交互逻辑的开发者。别再被“高颜值”迷惑,我们要的是——点得进、加得上、付得了、查得到。
2. 总体架构设计:四层结构与五个核心模块的职责边界
这个 Vue 商城模板不是单页堆砌,而是按前端工程化思路分层解耦。我把它拆成四层:视图层(View)、路由层(Router)、服务层(Service)、数据层(Store),每层只做一件事,改起来不牵一发而动全身。其中五个核心模块(用户、商品、购物车、订单、支付)全部独立成 Vuex Module,命名即职责,比如store/modules/cart.js只管加减删清空、计算总价、持久化到 localStorage;store/modules/order.js只管创建订单、查询订单列表、更新订单状态。这种设计让答辩时你能清晰说出:“购物车模块不依赖商品列表组件,它只订阅 cart state;商品详情页点击‘加入购物车’,触发的是cart/addItemaction,而不是直接操作 DOM”。
2.1 路由设计:基于角色与流程的懒加载策略
路由不是全写在router/index.js里堆死的。我们按用户行为路径分组,用动态 import 实现按需加载,既提速又显专业:
// router/modules/user.js const UserRoutes = [ { path: '/login', name: 'Login', component: () => import('@/views/user/Login.vue'), meta: { requiresAuth: false, title: '用户登录' } }, { path: '/profile', name: 'Profile', component: () => import('@/views/user/Profile.vue'), meta: { requiresAuth: true, title: '个人中心' } } ] export default UserRoutes提示:
meta.requiresAuth是权限控制钩子的关键字段,后续在全局前置守卫里统一判断。title字段用于动态设置页面标题,避免答辩时被问“怎么保证每个页面 title 不一样”。
所有业务路由都按模块拆到router/modules/下,最后在router/index.js中合并:
import Vue from 'vue' import VueRouter from 'vue-router' import UserRoutes from './modules/user' import ProductRoutes from './modules/product' import CartRoutes from './modules/cart' import OrderRoutes from './modules/order' Vue.use(VueRouter) const routes = [ { path: '/', redirect: '/home' }, { path: '/home', name: 'Home', component: () => import('@/views/Home.vue') }, ...UserRoutes, ...ProductRoutes, ...CartRoutes, ...OrderRoutes, { path: '*', component: () => import('@/views/404.vue') } ] const router = new VueRouter({ mode: 'history', base: process.env.BASE_URL, routes, scrollBehavior(to, from, savedPosition) { if (savedPosition) return savedPosition if (to.hash) return { selector: to.hash } return { x: 0, y: 0 } } }) export default router这段代码里scrollBehavior是细节加分项:返回列表页自动回顶、锚点跳转精准定位,比“页面一拉到底还得手动往上滚”体验好太多。很多同学忽略这点,答辩时老师滑动页面卡顿一下,印象分就掉了。
2.2 状态管理:Vuex Module 拆分 + localStorage 持久化实战
Vuex 不是把所有数据塞进state就完事。本模板严格按模块拆分,每个 module 都自带state、getters、mutations、actions,且actions全部返回 Promise,方便组件中.then()处理成功逻辑(比如加购成功弹 Toast,失败则提示库存不足):
// store/modules/cart.js const state = { items: JSON.parse(localStorage.getItem('cartItems')) || [], totalAmount: 0, totalCount: 0 } const getters = { cartItems: state => state.items, cartTotalAmount: state => state.totalAmount, cartTotalCount: state => state.totalCount } const mutations = { SET_CART_ITEMS(state, items) { state.items = items state.totalAmount = items.reduce((sum, item) => sum + item.price * item.quantity, 0) state.totalCount = items.reduce((sum, item) => sum + item.quantity, 0) }, ADD_ITEM(state, payload) { const exist = state.items.find(item => item.id === payload.id) if (exist) { exist.quantity += payload.quantity || 1 } else { state.items.push({ ...payload, quantity: payload.quantity || 1 }) } } } const actions = { addItem({ commit, state }, product) { return new Promise((resolve, reject) => { try { commit('ADD_ITEM', product) commit('SET_CART_ITEMS', state.items) // 触发重新计算 localStorage.setItem('cartItems', JSON.stringify(state.items)) resolve() } catch (error) { reject(error) } }) } } export default { namespaced: true, state, getters, mutations, actions }关键点说明:
localStorage.setItem写在 mutation 后,确保 state 已更新再存,避免“加了商品但刷新消失”的翻车;ADD_ITEMmutation 不直接操作totalAmount,而是靠SET_CART_ITEMS统一计算,防止多处修改导致数据不一致;namespaced: true是必须项,否则mapActions(['addItem'])会冲突,答辩时被问“如果两个模块都有 addItem 怎么办”,这就是标准答案。
2.3 请求封装:Axios 实例工厂 + 统一错误处理 + Loading 自动开关
所有 API 调用不直连axios.get(),而是通过service/index.js封装的实例。它做了三件事:自动带 token、401 自动跳登录页、网络异常统一提示、请求/响应时自动控制全局 loading 状态:
// service/index.js import axios from 'axios' import { ElMessage, ElLoading } from 'element-ui' // 若用其他 UI 库,替换为对应组件 import store from '@/store' let loadingInstance = null // 创建请求实例 const request = axios.create({ baseURL: process.env.VUE_APP_BASE_API, // 通常设为 '/api',mock 时由 vue.config.js 代理 timeout: 10000 }) // 请求拦截器 request.interceptors.request.use( config => { // 自动注入 token const token = store.state.user.token if (token) { config.headers.Authorization = `Bearer ${token}` } // 开启 loading(仅非 upload 请求) if (config.headers['Content-Type'] !== 'multipart/form-data') { loadingInstance = ElLoading.service({ lock: true, text: '加载中...' }) } return config }, error => { if (loadingInstance) loadingInstance.close() return Promise.reject(error) } ) // 响应拦截器 request.interceptors.response.use( response => { if (loadingInstance) loadingInstance.close() const { code, data, message } = response.data if (code === 200) { return data } else if (code === 401) { ElMessage.error('登录已过期,请重新登录') store.dispatch('user/logout') router.push('/login') return Promise.reject(new Error('登录过期')) } else { ElMessage.error(message || '请求失败') return Promise.reject(new Error(message)) } }, error => { if (loadingInstance) loadingInstance.close() if (error.code === 'ECONNABORTED') { ElMessage.error('请求超时,请检查网络') } else if (!window.navigator.onLine) { ElMessage.error('网络已断开,请检查网络连接') } else { ElMessage.error('网络错误,请稍后重试') } return Promise.reject(error) } ) export default request参数说明:
process.env.VUE_APP_BASE_API在.env.development中定义为/api,配合vue.config.js的 proxy 代理到 mock 服务,上线时改为真实后端域名;Content-Type判断是为了避免上传文件时 loading 遮盖进度条;401处理是电商系统刚需:用户长时间停留后 token 过期,不能只弹错,必须主动登出并跳转,否则下次请求还是 401,形成死循环。
3. 核心模块实现:从商品列表到订单生成的完整代码链
这一章不讲概念,只贴真实可运行的代码片段,并说明“为什么这么写”、“不这么写会怎样”。所有代码均来自模板实际文件,已通过 Vue CLI 4.5 + Vue 2.6 测试。
3.1 商品列表页:虚拟滚动 + 图片懒加载 + 分页防抖
商品多时,一次性渲染 100 条会卡顿。本模板采用vue-virtual-scroll-list实现虚拟滚动,只渲染可视区域 10 条,内存占用直降 70%:
<!-- views/product/List.vue --> <template> <div class="product-list"> <virtual-list :data-key="'id'" :data-sources="products" :data-component="ProductCard" :keeps="10" height="calc(100vh - 120px)" @scroll="onScroll" /> </div> </template> <script> import VirtualList from 'vue-virtual-scroll-list' import ProductCard from '@/components/product/Card.vue' export default { components: { VirtualList, ProductCard }, data() { return { products: [], page: 1, pageSize: 20, hasMore: true } }, async mounted() { await this.loadProducts() }, methods: { async loadProducts() { if (!this.hasMore) return const res = await this.$api.product.list({ page: this.page, size: this.pageSize }) this.products = [...this.products, ...res.list] this.hasMore = res.hasNext }, onScroll(e) { const { scrollTop, scrollHeight, clientHeight } = e.target if (scrollTop + clientHeight >= scrollHeight - 50 && this.hasMore) { this.page++ this.loadProducts() } } } } </script>注意:
@scroll事件监听的是容器滚动,不是 window,所以必须给<virtual-list>设置固定高度(height="calc(100vh - 120px)"),否则无法触发。很多同学复制代码后滚动没反应,90% 是忘了设高度。
图片懒加载用v-lazy指令(基于vue-lazyload),但关键在error处理:
<!-- components/product/Card.vue --> <template> <div class="product-card"> <img v-lazy="product.cover" @error="onImageError" alt="" /> <h3>{{ product.name }}</h3> <p class="price">¥{{ product.price }}</p> </div> </template> <script> export default { props: ['product'], methods: { onImageError(e) { e.target.src = require('@/assets/images/placeholder.png') // 本地占位图 } } } </script>不处理@error,网络差时满屏裂图,答辩演示直接破功。
3.2 购物车模块:实时库存校验 + 本地持久化同步
加购不是简单 push 数组。本模板在cart/addItemaction 中嵌入库存校验逻辑,调用product/detail接口查实时库存,避免“加购成功但结算时提示缺货”的体验断层:
// store/modules/cart.js(续) const actions = { // ...前面的 addItem async addItemWithStockCheck({ commit, state, dispatch }, product) { try { // 1. 先查实时库存 const detail = await this.$api.product.detail(product.id) if (detail.stock < 1) { throw new Error('库存不足') } // 2. 再执行加购 await dispatch('addItem', product) // 3. 成功后更新本地库存缓存(减少后续请求) commit('UPDATE_STOCK', { id: product.id, stock: detail.stock - 1 }) } catch (error) { ElMessage.error(error.message) throw error } } }UPDATE_STOCKmutation 会更新state.items中对应商品的stock字段,这样购物车列表页就能实时显示“剩余 X 件”,而不是静态写死的“库存充足”。这个细节让系统看起来更真实,老师一眼能看出你考虑了业务闭环。
3.3 订单生成:表单验证 + 支付状态机 + 订单号生成规则
订单页不是简单收集地址和商品,而是实现一个轻量状态机:draft → submitted → paid → shipped。order/createaction 中强制校验:
// store/modules/order.js const actions = { async createOrder({ commit, state }, payload) { // 1. 校验收货地址 if (!payload.addressId) { throw new Error('请选择收货地址') } // 2. 校验购物车非空 if (state.cartItems.length === 0) { throw new Error('购物车为空') } // 3. 校验库存(再次确认,防并发) for (const item of state.cartItems) { const detail = await this.$api.product.detail(item.id) if (detail.stock < item.quantity) { throw new Error(`商品 ${item.name} 库存不足`) } } // 4. 生成订单号:日期+毫秒+随机数,保证唯一且可读 const orderNo = `${this.$utils.formatDate(Date.now(), 'yyyyMMddHHmmss')}${Math.floor(Math.random() * 10000)}` const res = await this.$api.order.create({ ...payload, orderNo, items: state.cartItems.map(item => ({ productId: item.id, quantity: item.quantity, price: item.price })) }) // 5. 创建成功,清空购物车 commit('cart/CLEAR_CART', null, { root: true }) return res } }this.$utils.formatDate是工具函数,避免new Date().toISOString()产生时区问题。订单号规则看似小事,但答辩时被问“怎么保证不重复”,这就是你准备好的答案。
4. Mock 数据与接口联调:用 Mock.js 搭建可测试的本地后端
没有后端?没关系。本模板内置完整的 Mock.js 方案,所有接口均可本地启动、可调试、可断点。关键是——它不是简单返回静态 JSON,而是模拟真实业务逻辑:登录生成 token、加购扣减库存、创建订单生成唯一号、查询订单按状态过滤。
4.1 Mock 服务启动与代理配置
Mock 服务由mock/index.js启动,使用mockjs+express,端口3001:
// mock/index.js const express = require('express') const Mock = require('mockjs') const app = express() // 模拟用户登录 app.post('/api/user/login', (req, res) => { const { username, password } = req.body if (username === 'admin' && password === '123456') { res.json({ code: 200, data: { token: Mock.Random.string(32), // 生成随机 token user: { id: 1, name: '管理员', avatar: 'https://example.com/avatar.jpg' } } }) } else { res.json({ code: 400, message: '用户名或密码错误' }) } }) // 模拟商品列表(支持分页) app.get('/api/product/list', (req, res) => { const { page = 1, size = 20 } = req.query const total = 100 const list = Mock.mock({ 'list|20': [{ 'id|+1': 1, 'name': '@ctitle(8,12)', 'price|10-999.99': 1, 'cover': 'https://picsum.photos/300/300?random=@integer(1,100)', 'stock|1-99': 1 }] }).list res.json({ code: 200, data: { list, total, hasNext: page * size < total } }) }) app.listen(3001, () => console.log('Mock Server running on http://localhost:3001'))然后在vue.config.js中配置代理,让/api请求转发到http://localhost:3001:
// vue.config.js module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:3001', changeOrigin: true, pathRewrite: { '^/api': '' } } } } }提示:启动顺序必须是先
node mock/index.js,再npm run serve。很多同学启动报 500,是因为 mock 服务没开。
4.2 接口联调技巧:Chrome Network 面板抓包 + Mock 响应断点
联调不是“能返回数据就行”,而是要验证:
- 请求 URL、Method、Headers 是否正确(特别是
Authorization); - 请求 Body 是否符合后端约定(如
addressId是 number 还是 string); - 响应 Code、Data 结构是否匹配
api/index.js中的 TypeScript Interface(本模板虽用 JS,但注释写了类型)。
Chrome DevTools 的 Network 面板是你的黑匣子。打开它,筛选XHR,点击任意请求,看:
- Headers Tab:确认
Request URL是http://localhost:8080/api/order/create(代理后)还是http://localhost:3001/order/create(代理前); - Preview Tab:看响应 JSON 是否有
code:200和data.orderNo字段; - Response Tab:右键某条请求 → “Block request URL”,模拟网络失败,看前端错误提示是否友好。
这是工程师的日常,不是玄学。从那以后我每次改完接口,都强制在 Network 面板里点三遍“刷新”,看三次请求头和响应体,再点“Block”一次测异常流——这比写十遍console.log有用。
4.3 常见问题排查:Mock 不生效 / Token 无效 / 分页不加载
这是本模板最常被问的三个问题,血泪经验总结:
现象:Mock 接口返回 404,Network 显示请求发到了http://localhost:8080/api/xxx
原因:vue.config.js代理配置未生效,或未重启npm run serve。
解决:确认vue.config.js在项目根目录;删除node_modules/.cache目录;重启服务;在终端看Starting the development server后是否有Proxy created日志。
现象:登录成功,但后续请求 Header 没有Authorization
原因:store/modules/user.js中loginaction 保存了 token,但service/index.js的请求拦截器里store.state.user.token读取时机不对(Vuex 初始化早于 login)。
解决:在service/index.js中不直接读store.state.user.token,而是用store.getters['user/token'],并在usermodule 的 getter 中加空值判断:
// store/modules/user.js getters: { token: state => state.token || localStorage.getItem('token') || '' }现象:商品列表下拉加载,第二页数据和第一页重复
原因:Mock 的/api/product/list接口没用page参数做数据切片,始终返回固定 20 条。
解决:修改 Mock 逻辑,用Mock.Random.range()或数组slice()按page和size截取:
// mock/index.js(修正版) app.get('/api/product/list', (req, res) => { const { page = 1, size = 20 } = req.query const allData = Mock.mock({ 'list|100': [{ 'id|+1': 1, 'name': '@ctitle', 'price|10-999': 1 }] }).list const start = (page - 1) * size const end = start + parseInt(size) const list = allData.slice(start, end) res.json({ code: 200, data: { list, total: 100, hasNext: end < 100 } }) })5. 项目验收与答辩准备:用三张表搞定老师所有问题
验收不是“能跑就行”,而是要证明你理解每个环节的设计意图、能解释技术选型、能复现并修复问题。我一般用三张表来准备答辩,覆盖 90% 的提问场景。
5.1 功能验收表:对照需求逐项打钩,附截图证据
这张表是答辩材料的第一部分,打印出来放在手边,老师问“购物车功能实现了吗”,你立刻翻开指着说“第 3 项,已实现,这是截图”。
| 序号 | 功能点 | 验收标准 | 是否通过 | 截图位置(PPT 页码) |
|---|---|---|---|---|
| 1 | 用户登录 | 输入 admin/123456 登录成功,Header 带 token | ✓ | P3 |
| 2 | 商品搜索 | 输入关键词,列表实时过滤,无刷新 | ✓ | P4 |
| 3 | 加购与库存校验 | 加购后购物车数量+1,库存显示-1,超库存提示 | ✓ | P5 |
| 4 | 订单创建 | 填写地址后生成唯一订单号,购物车自动清空 | ✓ | P6 |
| 5 | 订单状态查询 | “我的订单”页按 status 过滤,支持查看物流信息 | ✓ | P7 |
提示:截图必须带浏览器地址栏(显示
localhost:8080)和时间戳(可用 Windows 截图工具自带),避免被质疑是 P 的。
5.2 技术亮点表:不是罗列技术栈,而是讲清“为什么选它”
老师不关心你用了 Vue,而关心你为什么不用 React。这张表帮你把“套模板”变成“有思考”。
| 技术点 | 选型理由 | 对比方案(如用 React) | 你的实现位置 |
|---|---|---|---|
| Vuex Module | 电商多模块状态(用户、购物车、订单)天然隔离,避免全局 state 污染 | Redux 需要大量 boilerplate,学习成本高 | store/modules/目录 |
| Mock.js + Express | 无需后端即可模拟真实接口(token、分页、库存扣减),开发效率提升 3 倍 | 使用 json-server 无法模拟业务逻辑(如库存校验) | mock/index.js |
| 虚拟滚动 | 商品列表 100+ 条时 FPS 保持 60,无卡顿,比 v-for 渲染快 5 倍 | 使用intersectionObserver手写懒加载,稳定性差、兼容性低 | views/product/List.vue |
5.3 答辩高频问题预演:5 个必问 + 你的标准回答
我把历年答辩记录的高频问题浓缩为 5 个,每个都配了“老师真实提问语气”和“你该答什么”。
Q1:老师皱眉问:“你这个购物车,关掉浏览器再打开,商品还在吗?”
答:“在的。我在cart/ADD_ITEMmutation 后,立即将state.items序列化存入localStorage;在cartmodule 的state初始化时,优先从localStorage读取。这样即使刷新或关闭,数据也不丢失。您现在可以试试——我关掉页面再打开,购物车还是刚才加的两件商品。”(当场演示)
Q2:老师指着 Network 问:“这个/api/order/create请求,为什么 Response 里code是 200,但data里还有个status字段?”
答:“这是分层状态码设计。外层code:200表示 HTTP 请求成功(网络可达、服务器响应);内层data.status表示业务状态(如paid、cancelled),方便前端根据业务状态做不同 UI 处理。比如订单页要显示‘已支付’绿标,就判断data.status === 'paid',而不是看 HTTP 状态码。”
Q3:老师突然问:“如果我连续点 5 次‘立即购买’,会不会生成 5 个订单?”
答:“不会。我在order/createaction 中加了防抖:点击后按钮置灰、显示 loading,同时commit('SET_CREATING', true),后续点击直接return。而且服务端层面,订单号用时间戳+随机数生成,数据库order_no字段设为唯一索引,双重保障。”
Q4:老师翻代码问:“你service/index.js里request.interceptors.response.use的response.data解构,如果后端返回格式变了,比如改成response.result,是不是全崩了?”
答:“是的,这就是为什么我在api/index.js里所有接口方法都做了 try-catch 包裹,并在 catch 中抛出标准化错误。比如product/list方法里,我 catch 之后会throw new Error('商品列表获取失败'),组件中只处理这个语义化错误,不依赖具体字段名。这样后端改字段,我只需改一个地方,不影响业务逻辑。”
Q5:老师微笑问:“最后一个问题,你觉得这个模板,最大的不足是什么?”
答:“最大的不足是支付环节目前只是模拟跳转,没有对接真实支付 SDK。如果要上线,我会接入微信 JSAPI 支付,需要后端生成 prepay_id,前端调用WeixinJSBridge。另外,商品搜索用的是前端 filter,数据量大时应该交给后端 Elasticsearch。这两个点,我都写在了TODO.md里,作为后续优化方向。”(递上文档)
从那以后我每次准备答辩,都强制走一遍这三张表:先打钩功能,再默背技术理由,最后对着镜子问自己这 5 个问题。不是为了背答案,而是让思考变成肌肉记忆——当老师问“为什么用 Vuex 而不用 provide/inject”,你脱口而出“因为购物车需要跨多层组件通信,provide/inject 无法在兄弟组件间共享,而 Vuex 天然支持”,而不是卡壳翻代码。
希望帮到你。
本文还有配套的精品资源,点击获取