拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Vue零食商城源码拆解:Pinia状态管理与购物车订单闭环

简介一款基于Vue框架的在线零食销售系统完整源码面向Web前端、Java后端开发者及电商系统学习者既可用于课程设计也可作为中小型零售业务的二次开发基础。系统采用Vue.js构建前端界面搭配Java后端与SQL脚本涵盖商品展示、购物车、订单管理、后台管理等典型电商模块。资源包共357个文件包括95个JavaScript逻辑文件、76个Vue组件、69个Java服务类、17个JSON配置、12个CSS样式、10张JPG及8张PNG图片等压缩包仅10.14MB目录结构清晰便于检索和部署。源码采用模块化设计前端包含admin管理端代码后端沉淀接口服务同时附带Markdown项目说明、开发环境配置、Excel数据样例及图标字体资源帮助读者快速理解架构、启动项目并扩展功能。对于希望掌握VueJava全栈实战、梳理在线交易系统逻辑的学习者而言是一份可运行、可拆解的高质量参考。已有120人学习下载。1. 为什么一套Vue零食销售系统源码比教学demo更值得拆一遍在线零食销售系统是Vue生态里相当典型的一类业务工程它把商品列表、购物车、用户登录、订单提交串成一个完整闭环既有前台店铺常见的展示交互又有购物车和订单这类强状态逻辑复杂度刚好卡在练手项目和真实上线项目之间。拆这套源码重点不是学一个页面怎么写而是看整个工程怎么组织路由怎么按页面拆、状态管理怎么放共享数据、HTTP层怎么统一处理、组件之间怎么用事件和数据流协作。这套方案适合刚学完Vue语法还缺一个完整项目的人也适合想在短时间内摸清陌生工程结构的老手。下面从选型开始把零食销售系统的设计思路、工程骨架和关键实现完整拆开。2. 技术选型和工程结构Vue 2与Vue 3怎么选、前后端如何分工2.1 选项式还是组合式接口风格决定读源码的第一印象拿到任何一套Vue源码我建议先打开package.json和src/main.js确认版本再决定按哪套API风格去理解代码。Vue 2.7版本之后虽然也支持组合式API但生态里大量组件库、插件和已有文档仍以Vue 2时代为主Vue 3配合Vite、Pinia和ant-design-vue这些新工具链开发体验和运行性能都有明显提升。零食商城这类业务量中等的前端工程两代Vue都能完成区别不在功能而在维护成本。新起项目我一般选Vue 3理由不是追新而是script setup语法糖大幅减少了模板与逻辑切换的成本组合式函数又能把购物车、订单这类强逻辑从组件里剥离出来后续改需求时定位代码更快。选项式的典型写法是把状态放进data、把方法写进methods通过this互相访问组合式则按逻辑关注点组织代码把同一类数据和操作收敛到一个函数里。零食商城里最容易看出差别的位置是购物车选项式写法下增删改查、数量加减、总价计算分散在computed和methods的不同区块组合式写法下这些收拢在一个useCart()函数里页面组件只负责把响应式数据和事件绑定到模板。下面这张表可以作为选型和阅读代码时的对照对比项选项式写法组合式写法代码组织按data/computed/methods分块按逻辑关注点拆函数逻辑复用依赖mixins命名容易冲突自定义组合函数天然隔离类型推导依赖this弱类型提示配合泛型与IDE提示更好上手难度低贴近传统写法需要先理解ref/reactive适用场景简单页面、老项目维护中后台和商城这类逻辑集中项目阅读老源码时两种写法都要能看懂但自己二次开发时建议优先组合式购物车和订单模块拆成组合函数后接新页面、加新能力的成本会明显下降。2.2 路由参数、导航守卫与组件懒加载商城页面层级不复杂首页、商品详情、购物车、结算、登录五个页面就能形成一个完整交易闭环但路由参数和导航守卫是必写的部分。商品详情页需要接收id参数结算页必须要求登录这两块分别对应动态路由和路由守卫。常见的路由组织方式如下// src/router/index.js import { createRouter, createWebHistory } from vue-router const routes [ { path: /, name: home, component: () import(/views/HomeView.vue) }, { path: /goods/:id, name: goods-detail, component: () import(/views/GoodsDetail.vue), props: true }, { path: /cart, name: cart, component: () import(/views/CartView.vue) }, { path: /checkout, name: checkout, component: () import(/views/CheckoutView.vue), meta: { requiresAuth: true } }, { path: /login, name: login, component: () import(/views/LoginView.vue) } ] const router createRouter({ history: createWebHistory(), routes }) // 全局前置守卫结算页未登录时跳转登录页并记录原目标 router.beforeEach((to, from, next) { const token localStorage.getItem(snack_token) if (to.meta.requiresAuth !token) { next({ name: login, query: { redirect: to.fullPath } }) } else { next() } }) export default router这段配置里有三个设计点值得细看。第一是组件懒加载() import()让Vue Router按路由维度切分代码块打开首页只加载首页的JS商品详情和结算页的代码等用户真正访问时才请求首屏体积明显变小。第二是路由参数透传/goods/:id是动态段配合props: true后页面组件里直接用defineProps([id])就能拿到商品ID阅读代码时不用来回找route.params.id。第三是meta标记在路由表里用meta.requiresAuth声明哪些页面需要登录守卫统一读取这个字段做拦截比在组件里逐个判断localStorage要清爽得多。2.3 Pinia管理购物车与登录态axios实例统一请求购物车是本项目共享状态最密集的区域。首页点击加入购物车写入数据导航栏显示角标数量购物车页修改数量结算页计算总价并提交订单四个地方读写同一份数据必须有一个集中状态层。在Vue 3源码里我推荐Pinia它比Vuex少一套mutations样板代码状态变更直接写在actions里类型推导也更友好。购物车store的典型实现// src/stores/cart.js import { defineStore } from pinia export const useCartStore defineStore(cart, { // 首次读取localStorage作为初始值实现刷新不丢数据 state: () ({ items: JSON.parse(localStorage.getItem(snack_cart) || []) }), 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) { const existed this.items.find(item item.id goods.id) if (existed) { existed.quantity 1 } else { this.items.push({ ...goods, quantity: 1 }) } this.persist() }, removeItem(id) { this.items this.items.filter(item item.id ! id) this.persist() }, persist() { localStorage.setItem(snack_cart, JSON.stringify(this.items)) } } })这里两个关键点。第一持久化策略state初始化时从localStorage恢复购物车每次增删后调用persist()写回刷新页面数据不丢。第二getters做聚合计算totalCount和totalPrice用reduce汇总模板里写cartStore.totalPrice即可响应式拿到总价不必在各个组件里重复写累加逻辑。items里存的是加入购物车那一刻的商品快照包含id、name、price、image购物车渲染时直接读快照字段避免再发一次详情请求。接口请求统一走axios实例这是商城源码里值得保留的设计习惯// src/utils/request.js import axios from axios import { message } from ant-design-vue const request axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器统一注入登录令牌 request.interceptors.request.use(config { const token localStorage.getItem(snack_token) if (token) { config.headers.Authorization Bearer ${token} } return config }) // 响应拦截器剥离data并统一处理401 request.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { localStorage.removeItem(snack_token) window.location.href /login } else { message.error(error.message || 请求失败) } return Promise.reject(error) } ) export default requestbaseURL设置为/api开发环境配合Vite的proxy转发到后端避免在代码里写死IP和端口。请求拦截器从localStorage取token注入到Authorization头响应拦截器统一剥离外层data业务代码直接拿到结果遇到401状态码时清除登录态并跳回登录页。后端如果基于Spring Boot这类框架做前后端分离这套封装的改动点通常只在baseURL和真实接口路径上。3. 环境配置与项目搭建让零食商城源码在本地跑起来3.1 先确认Node版本和包管理器再动安装依赖拿到源码先从根目录的package.json了解依赖然后确认本机Node版本。Vite 5要求Node.js 18以上npm 10同样依赖Node 18如果本机版本偏低安装依赖时会出现大量engine相关的警告甚至编译失败。我一般用nvm管理Node版本同一台机器上切换Vue 2的老项目常配Node 14/16和Vue 3的新项目Node 18/20时不会互相干扰。这里的经验是不要追最新的Node大版本选Vite官方长期支持的LTS版本更稳。网络环境影响npm install的速度和结果。国内网络环境下常见做法是配置npm镜像或用pnpm安装。安装依赖时如果出现ERESOLVE这类依赖冲突错误先检查是不是多个版本的Vue或ant-design-vue混在依赖树里优先统一版本号而不是直接加--force。3.2 创建Vue项目的命令与依赖安装顺序如果手上没有现成代码需要先创建一套零食商城的基础工程可以用Vite官方脚手架# 查看当前Node和npm版本确认环境可用 node -v npm -v # 用Vite创建Vue 3项目snack-shop是项目目录名 npm create vitelatest snack-shop -- --template vue # 进入项目并安装基础依赖 cd snack-shop npm install # 一次性安装商城需要的版本库路由、状态管理、HTTP库和UI组件库 npm install vue-router4 pinia axios ant-design-vue命令参数说明npm create vitelatest之后跟的是项目名-- --template vue表示生成Vue模板默认模板只包含Vite和Vue本体后续的npm install vue-router4 pinia axios ant-design-vue这四条依赖要自己补上。如果把项目从Vue 2迁移到Vue 3路由要用vue-router4而不是vue-router3写成npm install vue-router拿到的版本可能不对锁版本号是更可靠的写法。安装完依赖后在src/main.js里按顺序启用插件// src/main.js import { createApp } from vue import { createPinia } from pinia import Antd from ant-design-vue import ant-design-vue/dist/reset.css import App from ./App.vue import router from ./router const app createApp(App) app.use(createPinia()) app.use(router) app.use(Antd) // 全量引入开发调试期省事上线前可按需引入 app.mount(#app)这里app.use的注册顺序也有讲究Pinia先注册是因为路由守卫和业务组件里可能用到store路由注册后App.vue里的router-view才能正常渲染UI库放最后不影响前面两个。ant-design-vue全量引入在小项目里最省心代价是打包体积偏大上线前可以换成按需引入方案。3.3 vite.config.js里的别名、代理与端口设置vite.config.js是Vue 3工程里最常用的配置文件别名、开发代理、构建输出都在这改。下面是一份零食商城项目的常见配置// vite.config.js import { defineConfig } from vite import vue from vitejs/plugin-vue import { fileURLToPath, URL } from node:url export default defineConfig({ plugins: [vue()], resolve: { alias: { // 让 指向 src 目录组件引用路径更短 : fileURLToPath(new URL(./src, import.meta.url)) } }, server: { port: 5173, proxy: { // 开发环境把 /api 前缀的请求转发到后端解决跨域 /api: { target: http://localhost:8080, changeOrigin: true } } } })别名配置的作用很直接源码里到处都是/components/GoodsCard.vue如果不用别名就得写../../components/GoodsCard.vue目录调整一次就断一片引用。开发代理解决的是前后端分离联调时的跨域问题前端跑在5173后端跑在8080浏览器直接请求/api/goods会被跨域拦截配置proxy后Vite会把请求转发到http://localhost:8080/api/goods前端代码里不需要写http://localhost:8080这样的绝对地址。changeOrigin必须设为true让后端拿到的Host头是后端自己的域名否则部分框架的鉴权或者多租户路由会判断错。配置项作用常见问题server.port开发服务器端口被占用时换5174等未占用端口server.proxy接口转发与跨域changeOrigin忘了开后端拿错Hostresolve.alias路径别名配错导致模块找不到启动报错build.sourcemap生成源码映射生产默认关闭排查线上问题再开如果后端接口还没就绪本地调试可以在public下放一份mock/goods.json或者用mock插件拦截/api请求返回静态数据等后端联调时再切回真实代理。这种切换通常只要改vite.config.js里的一处配置不会污染业务代码。4. 零食销售系统核心模块实现商品、购物车、登录与订单闭环4.1 商品列表页的组件拆分与筛选逻辑商品列表页是用户进店看到的第一屏源码里一般会拆成页面容器、分类栏、商品卡片三个组件。页面容器负责请求数据和维护筛选条件分类栏只负责展示和派发切换事件商品卡片接收商品对象并派发加购事件。这种拆分让每个组件的职责单一后续加套餐满减这类新商品类型时改动范围能控制在少量组件内!-- src/views/HomeView.vue -- template div classhome !-- 分类栏通过 v-model 双向绑定当前分类 -- CategoryTabs v-modelactiveCategory / div classgoods-grid GoodsCard v-forgoods in filteredGoods :keygoods.id :goodsgoods add-carthandleAddCart / /div /div /template script setup import { ref, computed, onMounted } from vue import request from /utils/request import CategoryTabs from /components/CategoryTabs.vue import GoodsCard from /components/GoodsCard.vue import { useCartStore } from /stores/cart const cart useCartStore() const activeCategory ref(all) const goodsList ref([]) // 按选中分类过滤商品computed带缓存避免每次渲染重复计算 const filteredGoods computed(() { if (activeCategory.value all) return goodsList.value return goodsList.value.filter(item item.category activeCategory.value) }) // 页面挂载后请求商品数据 onMounted(async () { const { data } await request.get(/goods) goodsList.value data }) function handleAddCart(goods) { cart.addItem(goods) } /script这里的筛选逻辑放在computed里而不是模板中的函数调用里是因为computed有缓存用户切换分类时只有依赖的activeCategory变化才重新计算过滤结果。v-for渲染列表时:key必须取唯一值goods.id否则Vue在复用DOM节点时可能把图片和文本配对错误。组件通信链路是GoodsCard通过$emit(add-cart, goods)告诉父组件父组件再调用购物车store的addItem数据流是单向的排查问题时顺着这条链走就能定位。4.2 购物车数量加减、总价计算与store持久化购物车页面在源码里的核心是读store、改数量、算总价。模板部分直接消费useCartStore()的响应式数据所有变更都通过store的actions完成不在组件里直接修改cart.items!-- src/views/CartView.vue -- template div classcart-page div v-foritem in cart.items :keyitem.id classcart-row img :srcitem.image :altitem.name / div classgoods-info p{{ item.name }}/p p classprice单价 ¥{{ item.price.toFixed(2) }}/p /div div classquantity-box button clickcart.decrease(item.id)-/button span{{ item.quantity }}/span button clickcart.increase(item.id)/button /div p classsubtotal¥{{ (item.price * item.quantity).toFixed(2) }}/p button classremove clickcart.removeItem(item.id)删除/button /div div classcart-footer span共 {{ cart.totalCount }} 件合计 ¥{{ cart.totalPrice.toFixed(2) }}/span button clickgoCheckout去结算/button /div /div /template script setup import { useCartStore } from /stores/cart import { useRouter } from vue-router const cart useCartStore() const router useRouter() function goCheckout() { router.push(/checkout) } /scriptstore里需要补上数量加减这两个actions放在前面addItem和removeItem旁边// src/stores/cart.js increase(id) { const item this.items.find(item item.id id) if (item) { item.quantity 1 this.persist() } }, decrease(id) { const item this.items.find(item item.id id) if (item) { item.quantity - 1 // 数量减到0时直接移除该商品避免出现0件或负数 if (item.quantity 0) { this.items this.items.filter(i i.id ! id) } this.persist() } }价格计算有一个需要提前处理的坑浮点数累加会有精度误差比如0.1 0.2算出来是0.30000000000000004。零食单价和数量相乘时最稳的做法是后端把价格字段以分为单位返回整数前端展示时再除以100如果接口已经是元为单位的浮点数至少要在每个展示点都用toFixed(2)格式化并且把totalPrice也格式化成两位小数再显示。这个细节在订单金额比对上经常出问题做秒杀、满减这类活动时尤甚。4.3 登录注册、token持久化与订单状态流转登录注册是商城从浏览走向下单的必经环节。前端部分的核心是表单校验和token管理密码一致性校验、登录成功后保存token、跳回原浏览页面这三个操作对应一个完整的登录闭环// src/views/LoginView.vue 中的登录与注册逻辑 import { reactive } from vue import request from /utils/request import { message } from ant-design-vue import { useRouter, useRoute } from vue-router const router useRouter() const route useRoute() const form reactive({ username: , password: , confirmPassword: }) async function onLogin() { // 登录接口返回token成功后放入localStorage const { token } await request.post(/auth/login, { username: form.username, password: form.password }) localStorage.setItem(snack_token, token) // 登录前被守卫拦下时记录的目标地址有则跳回无则回首页 const redirect route.query.redirect || / router.push(redirect) } async function onRegister() { if (form.password ! form.confirmPassword) { message.error(两次输入的密码不一致) return } await request.post(/auth/register, { username: form.username, password: form.password }) message.success(注册成功请登录) router.push(/login) }登录后的订单状态是前后端共同维护的一组枚举。常见做法是前端只展示状态文案和对应的操作按钮状态流转由后端推进。一张基本的订单状态表如下状态含义前端可执行操作pending待支付取消订单、去支付paid已支付查看物流、申请退款shipped已发货确认收货completed已完成再次购买、评价cancelled已取消无下单结算时源码里通常会做两个动作把购物车里的商品快照复制一份作为订单明细然后清空购物车。商品快照的意义在于下单后商品价格变动不影响已生成的订单金额避免下单时10元结算时变15元的纠纷清空购物车则要放在订单创建成功的回调里不能先清后请求否则接口失败会导致数据丢失。5. 调试、验证与二次开发把源码改顺手的一些技巧5.1 用Vue Devtools检查组件状态和Pinia数据浏览器扩展商店装的Vue Devtools对调试这类商城源码帮助很大。打开Components面板可以看到当前页面组件树的props和state比如商品列表页的filteredGoods是怎么从goodsList过滤出来的实际数据对不对一目了然。Pinia面板里能直接看到cart store的items数组和totalPrice点击加入购物车后面板里的数据会同步变化不用再靠console.log逐个打断点。5.2 sourcemap辅助定位生产环境问题上线后如果线上报错堆栈全是压缩后的单行代码排查会非常吃力。自己控制部署环境时可以临时开启sourcemap构建npm run build -- --sourcemap或者直接在vite.config.js的build里设置sourcemap: true。这样构建产物会额外生成.map源码映射文件浏览器控制台报错时能映射回源码的文件和行号定位组件内部的状态变更和报错位置会快很多。需要留意的点是生产环境长期开着sourcemap会把源码暴露给访问者排查完问题及时关掉。5.3 二次开发的样式覆盖与接口扩展零食店铺的视觉调性偏暖色改主题色时与其到处找十六进制色值不如在一开始就定义CSS变量/* src/styles/theme.css */ :root { --primary-color: #ff6a00; /* 主色调零食店铺常用暖橙 */ --price-color: #e4393c; /* 价格强调色 */ --border-radius: 8px; }组件里需要覆盖ant-design-vue的默认样式时用:deep()选择器穿透scoped限制比如改按钮圆角就写.theme-btn :deep(.ant-btn) { border-radius: 8px; }。给源码扩展新接口时遵循已有的api模块划分在src/api下按资源区分文件商品、订单、用户各一个模块新接口写到对应文件里不要直接在组件内拼request.get。命令作用npm run dev启动开发服务器默认端口5173npm run build构建生产产物到dist目录npm run preview本地预览构建结果验证打包后效果npm run lint检查代码规范进仓库前跑一遍Vue Devtools里如果看不到Pinia面板先确认main.js里Pinia插件的注册顺序和Devtools版本是否匹配Vue 3工程配新版Devtools才能看到组合式API的完整状态。后续把库存校验、支付回调、订单超时这三个状态接上真实后端再在支付回调里补上库存扣减的幂等校验这套零食商城源码就能直接接上真实交易链路。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门