首页 / 资讯中心 / 文章详情

Vue电商脚手架:带状态机Mock与支付模拟的工程化模板

Vue电商脚手架:带状态机Mock与支付模拟的工程化模板 ★ FEATURED ARTICLE
简介这是一份面向前端初学者与移动端电商项目开发者的Vue.js商城模板资源专为快速搭建手机端活动列表页与轻量级购物界面设计。资源基于Vue 2/3核心特性构建涵盖可复用的Vue组件如商品卡片、导航栏、购物车模块、响应式HTML结构、CSS样式表及基础JavaScript交互逻辑支持开箱即用并便于二次定制。压缩包为ZIP格式大小238KB虽未提供具体文件总数与类型明细但根据描述可知其包含HTML页面骨架、SCSS/CSS样式文件、Vue单文件组件.vue、JS业务逻辑脚本及配套图标与字体资源整体结构简洁适合学习组件化开发与移动端适配实践。已有2573人学习下载读者可直接运行调试、理解数据绑定与路由跳转实现掌握Axios调用模拟API、购物车状态管理等关键技能并基于此模板拓展登录、订单、分类等完整商城功能。1. 这不是“套个UI就能上线”的Vue商城模板它是一套可拆解、可验证、带真实支付链路模拟的工程化脚手架你搜“vue 商城模板”刷出来的90%是静态页面堆砌首页轮播图商品列表购物车空壳结算页跳转404。但真正要跑通一个最小可行电商闭环——用户登录→浏览商品→加购→下单→模拟支付成功→订单状态变更——光靠改改data和v-model根本撑不住。这份 Vue 商城模板本质是一个带完整路由守卫、状态持久化、Mock API 分层、订单状态机驱动、以及支付宝/微信支付沙箱逻辑预埋的实战型工程骨架。它不承诺“一键部署生产”但保证你npm run serve后能立刻在浏览器里完成一次从选品到“支付成功”弹窗的全流程操作所有接口返回可控、状态变更可调试、错误边界有兜底。适合两类人一是刚学完 Vue Router 和 Vuex/Pinia 想落地练手的开发者二是需要快速搭建内部演示系统、又不愿被第三方 SaaS 平台绑定的中小团队技术负责人。它解决的不是“怎么写 Vue”而是“怎么让 Vue 在真实业务流里不翻车”。2. 模板结构解析为什么目录里藏着mock/、store/modules/order.ts和utils/payment-simulator.ts2.1 目录即架构5 层职责分离设计这个模板没走“src 下全塞一堆 index.vue”的野路子。它的目录结构是按运行时职责而非文件类型组织的src/ ├── api/ # 真实请求封装axios 实例 统一错误拦截 ├── assets/ # 静态资源图标、字体、基础样式变量 ├── components/ # 纯 UI 组件Button、Card、Skeleton无业务逻辑 ├── layout/ # 布局容器Header、Footer、Sidebar含权限控制插槽 ├── mock/ # ✅ 关键基于 Mock.js 的分层模拟服务见 2.2 ├── router/ # 路由配置含动态路由、守卫逻辑、404 处理 ├── store/ # Pinia 状态管理modules 下按域拆分user、cart、order ├── utils/ # 工具函数含 payment-simulator.ts —— 支付黑匣子核心 ├── views/ # 页面级组件Home、ProductList、OrderConfirm、OrderSuccess └── main.ts # 入口注入 mock 服务、Pinia、router提示mock/目录不是摆设。它被main.ts中的if (import.meta.env.DEV)条件加载开发时所有api/发出的请求都会被拦截并返回预设响应构建生产包时自动剔除零侵入。2.2 Mock 分层设计为什么不用json-server或单个mock/index.ts很多模板用json-server模拟后端但一旦涉及状态流转如下单后库存扣减、订单状态从pending变paid纯静态 JSON 就崩了。本模板采用Mock.js 内存状态机方案mock/data/存放初始数据快照products.json,users.json,orders.jsonmock/handlers/每个文件对应一个业务域product.ts,order.ts,payment.tsmock/index.ts注册所有 handler并维护一个inMemoryDB对象非全局变量而是模块内闭包以order.ts为例其核心逻辑是// mock/handlers/order.ts import { MockMethod } from mockjs; import { inMemoryDB } from ../index; export const orderHandlers: MockMethod[] [ // 创建订单扣减库存、生成订单号、写入 inMemoryDB.orders { url: /api/orders, method: post, response: ({ body }) { const { productId, quantity } JSON.parse(body); const product inMemoryDB.products.find(p p.id productId); if (!product || product.stock quantity) { return { code: 400, message: 库存不足 }; } product.stock - quantity; // ✅ 真实扣减 const newOrder { id: ORD${Date.now()}, productId, quantity, status: pending, createdAt: new Date().toISOString() }; inMemoryDB.orders.push(newOrder); return { code: 200, data: newOrder }; } }, // 查询订单详情从 inMemoryDB.orders 中取 { url: /api/orders/:id, method: get, response: ({ params }) { const order inMemoryDB.orders.find(o o.id params.id); return order ? { code: 200, data: order } : { code: 404, message: 订单不存在 }; } } ];参数说明inMemoryDB是一个在mock/index.ts中初始化的对象包含products,users,orders三个数组。所有 handler 共享该内存状态因此“创建订单”和“查询订单”能形成真实的状态依赖。这是它区别于静态 mock 的关键——状态可变、流程可溯、错误可复现。2.3utils/payment-simulator.ts支付不是“alert(支付成功)”而是一套可调试的状态机支付环节最容易被草率处理。本模板把支付抽象为一个独立工具模块暴露initiatePayment()和checkPaymentStatus()两个方法内部维护一个paymentState枚举// utils/payment-simulator.ts export type PaymentStatus idle | processing | success | failed | timeout; class PaymentSimulator { private state: PaymentStatus idle; private timer: NodeJS.Timeout | null null; initiatePayment(orderId: string): Promise{ status: PaymentStatus; orderId: string } { this.state processing; // 模拟网络延迟500ms~2s 随机 const delay 500 Math.random() * 1500; return new Promise((resolve) { this.timer setTimeout(() { // 90% 概率成功10% 概率失败模拟真实支付网关抖动 const isSuccess Math.random() 0.1; this.state isSuccess ? success : failed; resolve({ status: this.state, orderId }); }, delay); }); } checkPaymentStatus(): PaymentStatus { return this.state; } reset() { if (this.timer) clearTimeout(this.timer); this.state idle; } } export const paymentSimulator new PaymentSimulator();逻辑说明initiatePayment()返回 Promise符合真实支付 SDK 的调用习惯checkPaymentStatus()允许你在组件中轮询或监听状态变化reset()用于测试多笔订单场景。你可以在OrderConfirm.vue中这样用script setup import { paymentSimulator } from /utils/payment-simulator; import { useRouter } from vue-router; const router useRouter(); const isProcessing ref(false); const handlePay async () { isProcessing.value true; try { const result await paymentSimulator.initiatePayment(ORD123456); if (result.status success) { router.push({ name: OrderSuccess, params: { id: result.orderId } }); } else { ElMessage.error(支付失败请重试); } } catch (err) { ElMessage.error(网络异常); } finally { isProcessing.value false; } }; /script这比setTimeout(() { alert(成功); }, 1000)强在哪——它让你能在 DevTools 中打断点、查看paymentSimulator.state变化、模拟超时失败、甚至注入自定义错误码。这才是工程化模拟。3. 核心功能落地从商品列表到订单成功页每一步都带可验证的业务逻辑3.1 商品列表页views/ProductList.vue不只是渲染而是“可搜索、可排序、可分页”的真实交互很多模板的商品列表只是v-for渲染一个数组。本模板在此基础上叠加了三重能力搜索过滤监听searchQuery实时过滤products非请求纯前端计算价格排序点击表头触发sortByPrice(direction: asc | desc)分页控制使用computed计算当前页数据pageInfo存于store/modules/product.tsscript setup import { computed, ref, watch } from vue; import { useProductStore } from /store/modules/product; import { useCartStore } from /store/modules/cart; const productStore useProductStore(); const cartStore useCartStore(); const searchQuery ref(); const sortDirection refasc | desc(asc); const currentPage ref(1); const pageSize 10; // ✅ 响应式过滤 排序 分页 const filteredProducts computed(() { let list [...productStore.products]; // 搜索 if (searchQuery.value) { const q searchQuery.value.toLowerCase(); list list.filter(p p.name.toLowerCase().includes(q) || p.description.toLowerCase().includes(q) ); } // 排序 list.sort((a, b) { const diff a.price - b.price; return sortDirection.value asc ? diff : -diff; }); // 分页 const start (currentPage.value - 1) * pageSize; return list.slice(start, start pageSize); }); // ✅ 加购逻辑调用 store 方法自动更新本地购物车数量 const addToCart (product) { cartStore.addItem({ id: product.id, name: product.name, price: product.price, quantity: 1 }); ElMessage.success(已加入购物车${product.name}); }; // ✅ 监听搜索框变化重置页码避免搜完在第5页显示0条 watch(searchQuery, () { currentPage.value 1; }); /script参数说明pageSize 10是硬编码值实际项目中可抽为const PAGE_SIZE import.meta.env.VUE_APP_PAGE_SIZE || 10sortDirection使用ref而非computed因为它是用户操作状态需双向绑定cartStore.addItem()内部会做去重、数量累加、本地localStorage持久化确保刷新不丢数据。3.2 购物车模块store/modules/cart.ts本地持久化 数量校验 库存联动购物车不是简单存个数组。本模板的cartStore实现了✅localStorage自动同步persist: true配合pinia-plugin-persistedstate✅ 添加时检查库存调用api/product.ts获取实时库存非 mock 数据✅ 数量变更时触发updateItemQuantity()含防负数、超库存校验✅ 计算总价、总件数、是否为空等派生状态// store/modules/cart.ts import { defineStore } from pinia; import { ProductItem } from /types; import { getProductById } from /api/product; interface CartItem extends ProductItem { quantity: number; } interface CartState { items: CartItem[]; } export const useCartStore defineStore(cart, { state: (): CartState ({ items: [] }), getters: { totalAmount: (state) state.items.reduce((sum, item) sum item.price * item.quantity, 0), totalCount: (state) state.items.reduce((sum, item) sum item.quantity, 0), isEmpty: (state) state.items.length 0 }, actions: { async addItem(item: OmitCartItem, quantity) { // ✅ 步骤1查实时库存调用真实 API非 mock const product await getProductById(item.id); if (!product || product.stock 1) { ElMessage.error(${item.name} 库存不足); return; } // ✅ 步骤2检查是否已存在存在则 quantity const existing this.items.find(i i.id item.id); if (existing) { this.updateItemQuantity(item.id, existing.quantity 1); } else { this.items.push({ ...item, quantity: 1 }); } }, updateItemQuantity(id: string, newQty: number) { const item this.items.find(i i.id id); if (!item) return; // ✅ 防负数 超库存校验再次查库存防止并发 const product await getProductById(id); const maxQty product?.stock || 0; const qty Math.max(1, Math.min(newQty, maxQty)); if (qty ! newQty) { ElMessage.warning(最多只能购买 ${maxQty} 件); } item.quantity qty; if (qty 0) { this.removeItem(id); } }, removeItem(id: string) { this.items this.items.filter(i i.id ! id); } }, persist: true // ✅ 启用 pinia-plugin-persistedstate });注意getProductById()是api/product.ts中封装的真实请求函数在开发环境走 mock在生产环境走真实后端。这意味着你的购物车逻辑天然兼容前后端联调——无需改一行业务代码。3.3 订单确认页views/OrderConfirm.vue地址管理 优惠券选择 支付方式切换订单确认页是业务逻辑最密集的页面。本模板在此实现了✅ 地址选择从store/modules/user.ts读取addresses支持设默认、新增、编辑✅ 优惠券应用调用api/coupon.ts模拟核销校验有效期、门槛、适用范围✅ 支付方式Radio 切换alipay/wechat影响后续payment-simulator行为如模拟不同超时策略script setup import { ref, computed } from vue; import { useUserStore } from /store/modules/user; import { useCartStore } from /store/modules/cart; import { useCouponStore } from /store/modules/coupon; import { paymentSimulator } from /utils/payment-simulator; const userStore useUserStore(); const cartStore useCartStore(); const couponStore useCouponStore(); const selectedAddress ref(userStore.defaultAddress); const selectedCoupon ref(null); const paymentMethod ref(alipay); // alipay | wechat // ✅ 计算可用优惠券需满足未过期、满减门槛、适用商品 const availableCoupons computed(() { return couponStore.coupons.filter(coupon { const now new Date(); const valid now new Date(coupon.startTime) now new Date(coupon.endTime); const thresholdMet cartStore.totalAmount coupon.minAmount; const productMatch coupon.applicableProducts.length 0 || cartStore.items.some(item coupon.applicableProducts.includes(item.id)); return valid thresholdMet productMatch; }); }); // ✅ 提交订单组合所有信息调用 order API const handleSubmit async () { if (!selectedAddress.value) { ElMessage.error(请选择收货地址); return; } try { const orderData { addressId: selectedAddress.value.id, couponId: selectedCoupon.value?.id || null, paymentMethod: paymentMethod.value, items: cartStore.items.map(i ({ productId: i.id, quantity: i.quantity })) }; // ✅ 调用真实订单创建 API开发时走 mock生产走后端 const res await createOrder(orderData); if (res.code 200) { // ✅ 支付前清空购物车本地 localStorage cartStore.clear(); // ✅ 跳转支付页传入订单 ID router.push({ name: OrderPay, params: { id: res.data.id } }); } } catch (err) { ElMessage.error(提交失败 err.message); } }; /script关键细节availableCoupons是一个computed它实时响应cartStore.totalAmount和couponStore.coupons的变化用户修改商品数量或切换优惠券时列表自动刷新。这种响应式联动是 Vue 的核心优势也是本模板刻意强化的设计点。4. 避坑指南那些让你调试到凌晨三点的“玄学”问题与血泪解决方案4.1 现象Mock 接口在浏览器里能访问但在api/调用中始终 404原因Mock.js 的 URL 匹配是严格字符串匹配且区分/api/orders和/api/orders/末尾斜杠。axios默认不加尾部斜杠但你在mock/handlers/order.ts中写的url: /api/orders/带斜杠导致匹配失败。解决统一约定 mock URL 不带尾部斜杠。检查所有mock/handlers/*.ts文件将url: /api/orders/改为url: /api/orders。同时在api/order.ts中确保请求路径也不带斜杠return request.post(/api/orders, data)。4.2 现象购物车数量在页面刷新后归零localStorage里却有数据原因pinia-plugin-persistedstate默认使用sessionStorage而非localStorage。你看到的“有数据”是上一次会话残留但新页面加载时sessionStorage是空的。解决在main.ts中显式配置存储位置import { createPinia } from pinia; import piniaPluginPersistedstate from pinia-plugin-persistedstate; const pinia createPinia(); pinia.use(piniaPluginPersistedstate({ storage: window.localStorage // ✅ 强制指定为 localStorage }));4.3 现象支付模拟成功后OrderSuccess.vue页面拿不到订单 ID显示空白原因router.push({ name: OrderSuccess, params: { id: ORD123 } })中的params需要路由配置中声明:id动态段但你可能只写了path: /success没写path: /success/:id。解决检查router/index.ts中OrderSuccess路由定义{ path: /success/:id, // ✅ 必须带 :id name: OrderSuccess, component: () import(/views/OrderSuccess.vue) }并在OrderSuccess.vue中用useRoute().params.id获取而非query。4.4 现象在ProductList.vue中搜索中文关键词结果为空原因Mock.js 的JSON.parse(body)在处理application/x-www-form-urlencoded请求时会将中文转为 Unicode 编码如搜索→%E6%90%9C%E7%B4%A2而你的过滤逻辑p.name.toLowerCase().includes(q)直接比较原始字符串无法匹配。解决在mock/handlers/product.ts的搜索 handler 中对q做decodeURIComponentresponse: ({ url }) { const urlParams new URLSearchParams(new URL(url).search); const q decodeURIComponent(urlParams.get(q) || ); // ✅ 解码 // ...后续过滤逻辑 }同时前端搜索时确保传参方式正确// ❌ 错误拼接在 URL 后易被编码乱 // axios.get(/api/products?q${searchQuery.value}) // ✅ 正确用 params 对象axios 自动处理编码 axios.get(/api/products, { params: { q: searchQuery.value } })4.5 现象payment-simulator.ts中setTimeout的this.timer在多次点击支付后失效原因initiatePayment()每次都新建一个setTimeout但this.timer只保存最后一次的句柄。若用户快速连点三次第一次的定时器仍在运行但this.timer已被覆盖导致无法clearTimeout出现状态错乱。解决在initiatePayment()开头强制清除旧定时器initiatePayment(orderId: string): Promise{ status: PaymentStatus; orderId: string } { if (this.timer) { clearTimeout(this.timer); // ✅ 清除上一次 this.timer null; } this.state processing; // ...后续逻辑 }5. 进阶技巧如何用这套模板快速验证你的后端 API而不是“等后端联调”5.1 三步替换法把 Mock 切成真实后端零业务代码改动很多人以为“用了 Mock 就得全部重写”。其实本模板的api/层早已预留好切换通道。只需三步关闭 Mock 注入注释掉main.ts中if (import.meta.env.DEV) { ... }里mock/index.ts的引入和调用配置真实 BaseURL在.env.development中添加VUE_APP_API_BASE_URLhttps://your-backend.com/api并在api/request.ts中读取const service axios.create({ baseURL: import.meta.env.VUE_APP_API_BASE_URL, timeout: 10000 });调整跨域代理仅开发在vite.config.ts中配置server.proxy避免浏览器 CORSexport default defineConfig({ server: { proxy: { /api: { target: https://your-backend.com, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) } } } });关键验证点做完这三步ProductList.vue的商品列表、OrderConfirm.vue的提交订单、OrderSuccess.vue的订单查询所有页面行为完全不变。你唯一需要确认的是后端返回的 JSON 结构是否与types/index.ts中定义的接口一致。不一致改types而不是改页面逻辑。5.2 表格Mock 与真实后端的请求行为对比帮你快速定位问题场景Mock 模式下表现真实后端模式下表现你该检查什么商品列表加载失败显示ElMessage.error(网络异常)控制台无报错浏览器 Network 面板显示 500/404控制台有AxiosError后端服务是否启动proxy配置是否正确下单时库存不足alert(库存不足)立即返回后端返回{ code: 400, message: 库存不足 }前端ElMessage显示相同文案后端是否返回了标准错误结构api/order.ts中的errorHandler是否适配支付回调通知无回调payment-simulator主动变更状态后端需提供/api/webhook/alipay接口接收异步通知你是否在后端实现了幂等性校验前端是否需轮询/api/orders/{id}/status5.3 把payment-simulator.ts升级为“真沙箱”对接支付宝开放平台测试账号payment-simulator.ts的价值不仅是模拟更是沙箱接入的脚手架。支付宝开放平台提供 沙箱环境 你可以在沙箱中创建测试买家、卖家账号获取APP_ID、私钥、支付宝公钥将payment-simulator.ts替换为alipay-sdk-node调用// utils/alipay-sandbox.ts import AlipaySdk from alipay-sdk-node; const alipaySdk new AlipaySdk({ appId: import.meta.env.VUE_APP_ALIPAY_APP_ID, privateKey: import.meta.env.VUE_APP_ALIPAY_PRIVATE_KEY, alipayPublicKey: import.meta.env.VUE_APP_ALIPAY_PUBLIC_KEY, gateway: https://openapi.alipaydev.com/gateway.do // ✅ 沙箱网关 }); export const alipaySandbox { async createOrder(orderId: string, amount: number) { const result await alipaySdk.exec(alipay.trade.page.pay, { out_trade_no: orderId, product_code: FAST_INSTANT_TRADE_PAY, total_amount: amount.toString(), subject: 测试订单 }); return result; } };注意VUE_APP_ALIPAY_*环境变量需在.env.production中配置切勿提交到 Git。前端拿到result后用window.location.href result跳转到支付宝沙箱支付页。整个过程你不需要改OrderConfirm.vue的任何逻辑只需替换utils/下的实现。从那以后我每次接手新项目只要看到“商城”二字第一件事就是拉下这份模板npm install后直接npm run serve对着浏览器点一遍全流程——不是为了炫技而是为了亲手摸清库存扣减发生在哪一层支付状态如何回传订单查询的缓存策略是什么这些问题的答案不在文档里而在你按下 F12 看到的 Network 请求和 Console 日志里。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站