ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Vue3的校园二手交易系统开发实战:从Composition API到前后端联调

基于Vue3的校园二手交易系统开发实战:从Composition API到前后端联调 校园二手交易这个项目我前前后后带过几届学生做毕设也自己完整重构过一版算是踩过不少坑。拿到“基于Vue3的校园二手商品交易系统设计与实现”这个题目时很多人第一反应是“不就是个增删改查嘛”但真做起来才会发现从需求拆分到前端架构再到前后端联调每一步都有讲究。这篇文章我尽量把整个从0到1的过程展开来讲包括为什么用Vue3、Composition API怎么组织代码、商品发布和订单流程怎么设计、Vite和Axios怎么配以及我实际开发中踩过的那些莫名其妙的坑。1. 项目需求拆解与系统整体架构1.1 校园二手场景的特殊性分析做校园二手交易系统第一件事不是写代码而是搞清楚“校园”这两个字到底意味着什么。跟闲鱼、转转这种面向全社会的二手平台相比校园场景有几个非常明显的差异这些差异会直接影响你的表结构设计和前端页面规划。校园用户的交易半径极小基本锁定在同一个校区甚至同一栋宿舍楼内。这意味着系统不需要复杂的物流追踪只需要一个“见面交易”的约定流程。所以订单模块里的配送方式基本可以简化为“校内自提”和“约定地点”这大大降低了开发的复杂度。用户身份相对可信。学生群体有学号、院系信息可以通过学生证或在读信息做基础认证。相比全社会平台要求的个人身份认证校园系统只需要绑定学号就能完成信任基础的搭建。前端登录页面可以设计成“学号密码”或“手机号密码”两种方式后者更利于扩展到其他校园服务。交易时间有强烈的周期性。开学季教材、宿舍用品需求暴增毕业季大量闲置物品集中释放。这就是为什么后台管理需要数据统计功能按时间段看发布量与成交量的趋势对运营和功能迭代都有帮助。二手商品的品类高度集中。教材教辅、电子产品、生活用品、运动器材、宿舍神器这五类基本能覆盖绝大多数闲置。前端分类导航按这个维度设计搜索和筛选时体验会好很多。明确了这四点系统的功能边界其实就清晰了前台以商品发布、浏览、搜索、详情、下单、个人中心为核心闭环后台以用户管理、商品审核、订单管理、数据统计为核心。会员积分这类花哨功能一律砍掉优先保证主流程的完整性和可用性。1.2 为什么选Vue3而不是Vue2技术选型处处是学问不是说Vue2不能做而是Vue3在几个关键点上的优势对这个项目来说性价比更高。组合式API的代码组织方式解决的是业务逻辑复用和可维护性的问题。校园二手系统的核心是商品、订单、用户三个域如果用Vue2的Options API每个组件里data、methods、computed各占一块同一个商品审核的逻辑会被拆散在不同配置项里。而Vue3的setup语法让同一个功能的变量和方法可以放在一起配合自定义组合式函数比如useGoods、useOrder代码的阅读成本和维护成本都会明显降低。尤其是做订单状态切换这种稍微有点逻辑的模块体验差距非常明显。响应式系统的底子换了。Vue3用Proxy替代了Vue2的Object.defineProperty这意味着可以直接监听属性的新增和删除处理数组下标赋值也更准确。做交易系统时订单状态从“待付款”改成“已确认”或者给商品对象动态挂一个收藏标记这类操作在Vue3里不会出现“数据变了但视图没反应”的玄学问题。生态已经完全成熟。Element Plus、Vant、Pinia都支持Vue3Vite的冷启动速度比webpack快一个量级。对做毕设或练习项目的人来说这能省下大量等待时间把精力花在业务逻辑上。我经常跟人说如果只是想应付一个页面展示Vue2确实够用。但“系统设计与实现”这个题审稿老师看的是你的设计能力和工程化思维。Vue3 Composition API Pinia Vite这套组合本身就是项目设计里一个可以写在文档里的加分项。1.3 系统模块拆分与前端目录组织动手写代码前先把前端目录结构定好。我个人习惯按“功能模块 业务域”来组织而不是单纯按文件类型堆。一个可供参考的目录结构如下src/ ├── api/ # 接口请求层 │ ├── goods.js │ ├── order.js │ ├── user.js │ └── auth.js ├── assets/ ├── components/ # 通用组件 │ ├── GoodsCard.vue │ ├── UploadImage.vue │ └── OrderStatusTag.vue ├── composables/ # 组合式函数 │ ├── useGoodsList.js │ ├── useOrderFlow.js │ └── useAuth.js ├── router/ ├── stores/ # Pinia状态 │ ├── user.js │ └── app.js ├── views/ │ ├── home/ │ ├── goods/ │ ├── order/ │ ├── user/ │ └── admin/ └── utils/这个结构的关键点是composables和stores的分离。composables里放的是“怎么处理数据”的逻辑比如调用接口、处理分页参数、计算筛选条件stores里放的是“跨页面共享的数据”比如登录用户信息、系统配置。把这两层分开页面组件的职责就被压缩得很薄只负责模板渲染和事件分发维护起来非常轻松。后端接口设计上电商类系统的标准接口可以按照资源来拆分登录注册、用户信息、商品发布、商品列表、商品详情、商品状态修改、订单创建、订单状态流转、后台数据统计。每类接口的参数和返回值保持稳定前端Api层的方法跟后端Controller一一对应。2. 核心技术点详解Vue3在项目中怎么用才到位2.1 Composition API的编排思路很多初学Vue3的人都会疑惑一个问题明明Option API写起来也挺顺手的setup里加了一堆ref和function反而感觉更乱。其实这是没有掌握正确的编排姿势。Composition API的精髓不是把data变成ref而是按逻辑关注点组织代码。举个例子商品列表页涉及的东西有搜索表单数据、分页状态、列表数据、加载状态、筛选条件、排序规则。如果用Option API这些相关的东西会被塞进data、methods、computed三个区域上下翻找非常拧巴。用setup后我习惯这样组织const searchForm ref({ keyword: , category: , minPrice: }) const pageInfo reactive({ page: 1, pageSize: 12, total: 0 }) const goodsList ref([]) const loading ref(false) async function fetchGoods() { loading.value true try { const { list, total } await goodsApi.getList({ ...searchForm.value, ...pageInfo }) goodsList.value list pageInfo.total total } finally { loading.value false } } function handleSearch() { pageInfo.page 1 fetchGoods() } function handlePageChange(page) { pageInfo.page page fetchGoods() }这一组代码放在一起页面的核心业务流程一目了然。更复杂的场景可以把这部分逻辑抽成组合式函数放到composables/useGoodsList.js里让组件只保留一句话调用页面代码清爽很多。2.2 ref和reactive的使用选择Vue3里声明响应式数据有ref和reactive两套方案我见过不少项目用得很混乱这里给一个比较实用的判断标准基础类型的值用ref对象和数组类型的结构化数据用reactive但如果你需要整体替换这个数据比如接口返回后直接赋值一个全新的数组那优先用ref。原因在于reactive返回的是原始对象的Proxy如果你在函数里写成goodsList.value res.data这种赋值直接把整个对象换掉的话会丢失响应式而ref通过.value的代理来包装整体赋值是安全的。这个坑在商品列表页里最容易出现从接口拿回新数组赋值给reactive声明的变量页面死活不更新查了半天才发现是这个原因。有一个细节值得单独说如果你用ref声明一个对象类型的变量比如商品表单数据const form ref({})在模板里用的时候别忘记.value在脚本里操作也一样。为了少写两个字符写成了const form reactive({...})但后续代码里又出现form.value.name这种写法就会直接报错这种代码风格混乱问题在多人协作时尤其明显。响应式数据的性能问题也要注意。商品列表如果有几百上千条数据每一条都是一个深层的响应式对象修改其中一条的某个属性会触发大量依赖更新。实际项目中可以给卡片列表用shallowRef或shallowReactive或者干脆不要每一层都做响应式只让列表本身是响应式的铺开渲染时对性能改善非常明显。2.3 状态管理用Pinia怎么说都得更合适Vue3项目里状态管理基本默认Pinia对比Vuex有几个非常实际的体验优势没有mutations直接在actions里改状态代码量直接少了一半对TypeScript的支持程度更高做稍微规范一点的项目会舒服很多store之间可以相互调用不需要像Vuex那样通过rootState绕来绕去。交易系统里哪些数据需要全局共享登录用户信息、搜索历史可选、未读数等。以用户信息为例登录完成后把用户对象存到store中所有需要显示用户名的组件直接从store里取不需要走props一层层传递修改头像和昵称后全站同步更新。Pinia的store设计也很直白// stores/user.js import { defineStore } from pinia export const useUserStore defineStore(user, { state: () ({ token: localStorage.getItem(token) || , userInfo: {} }), getters: { isLoggedIn: (state) !!state.token, userName: (state) state.userInfo?.name || 未登录 }, actions: { setToken(token) { this.token token localStorage.setItem(token, token) }, setUserInfo(info) { this.userInfo info } } })刷新页面时token从localStorage里恢复用户信息在App.vue的onMounted里调一次接口拉回来这个过程对用户是无感知的。对比一下Vuex的写法少了mutation的样板代码之后整个store的阅读体验和写代码的顺畅度都是明显提升的。2.4 组件通信方案的选择组件通信选对方案能让代码优雅很多。props向下传、emit向上传是基础但在交易系统里有两个场景我建议用provide/inject来处理比层层传prop省太多事。第一个场景是登录状态的全局传递。Layout组件在setup里通过provide注入userStore子组件商品列表、导航栏、个人中心都能直接读取不需要每一层都接收一个userInfo的prop。第二个场景是通用弹窗组件的控制比如商品删除确认弹窗、订单取消确认框父组件provide一个openConfirm方法子组件inject后直接用这种“方法注入”比props传函数然后emit回调要清爽。页面上大量商品卡片、订单列表项这类数据展示组件建议props只传必要的数据对象不要在props里传方法。方法通过emit上抛到父组件统一处理保持单向数据流后续接到后端复杂逻辑时调试链路会清晰很多。我还习惯给这类组件单独抽一个状态标签子组件比如订单状态用不同颜色的tag展示会让页面层次感好看很多。3. 实操过程前端核心模块从0到13.1 登录注册与用户体系搭建登录模块看起来简单但它是整个系统的入口认证流程设计不好后面全乱。前端需要做的事包括登录表单校验、调用登录接口、存储token、跳转回原页面。我用的是token方案JWT或者简单token都可以后端返回token后在本地存储一份每次请求带上后端的拦截器负责校验登录态。登录成功后跳回来源页面这个细节很多人忽略。用户浏览商品详情页时登录过期被重定向到登录页登录完直接跳回首页显然很蠢。我的做法是在路由守卫里记住目标路由登录成功后再跳过去router.beforeEach((to, from, next) { const userStore useUserStore() if (to.meta.requiresAuth !userStore.isLoggedIn) { next({ path: /login, query: { redirect: to.fullPath } }) } else { next() } })注册页面的设计上除了学号和密码之外我还建议带一个院系选择下拉框虽然不算核心功能但是对后端的用户数据统计分析有帮助也能让审核员在后台看到更完整的用户信息。表单校验这块用Element Plus自带的rules就够用了正则校验学号格式、手机号格式密码长度设置在6到20位之间前端校验只是第一道防线后端接口必须有同样的校验逻辑。3.2 商品发布与图片上传实现商品发布是二手交易系统的核心交互场景表单字段至少要包括标题、描述、分类、价格、原价可选、成色、交易方式、图片。前端用Form组件加rules做校验标题限制20字以内超过就不让提交这是为了列表展示时的体验。描述建议做成纯文本区域不要引入富文本编辑器二手商品描述普遍不长富文本的复杂度和安全风险都划不来。图片上传这块有几个实际细节要处理好。第一个是一次连续上传多张图片而不是让用户一张一张传。我用el-upload的file-list配合v-model绑定图片URL数组上传成功后往数组里push删除时同步从数组中移除。第二个是图片压缩手机端拍的照片动不动好几兆直接传既慢又占服务器空间。方案是在上传前用canvas压缩一下长边控制在1280px质量调到0.8体积能降下来80%以上肉眼看起来跟原图几乎没差别。第三是为每张图片生成一个本地临时ID用于前端预览上传成功后再替换成服务器返回的URL避免上传过程中显示空白。发布接口的返回机制也很讲究。后端要设计成“提交后进入待审核状态”而不是直接上架。学生用户发布的商品可能包含违规内容需要一个审核岗。前端发布成功后的跳转页面我建议直接带到“我的发布”列表并且清晰标注“审核中”状态让用户知道自己的操作有没有被系统接收。3.3 商品列表、搜索与筛选商品列表是整个系统流量最大的页面性能和体验要重点打磨。搜索区采用“关键词 分类 价格区间”三个维度关键词走商品标题和描述模糊匹配分类是精确筛选价格区间可以是两个输入框也可以是一个Range组件。筛选后的排序支持最新发布、价格从低到高、价格从高到低三种默认按发布时间倒序。列表页的分页设计上因为商品数量一般不会太大后端可以做简单的分页或者更轻量的滚动加载。滚动加载更符合用户浏览习惯但实现复杂度稍高一点需要处理好滚动容器的监听和防抖。我建议做分页为主因为毕设文档里写清楚PageHelper或MyBatis-Plus的分页逻辑比滚动加载更容易体现后端功底。卡片组件的设计上每个商品卡片用GoodsCard.vue统一渲染展示第一张图片、标题、价格、成色、发布时间。点击卡片跳转到详情页跳转方式用RouterLink。价格文字的样式要突出红色加粗是二手交易平台的标配原价用删除线展示品牌感和信息层级都靠这些细节撑起来。详情页还有一个关键点浏览量的统计。每次进入详情页时调用一次浏览量加1的接口这个数据在后台上可以看到也可以作为排序的一个选项。技术上很简单但设计文档里有这个细节会让系统显得完整不少。3.4 订单流程与状态管理订单模块是校验系统全面性的点。二手交易和电商还不完全一样它会多一个“线下交付确认”的环节。我设计的订单状态机是待付款 → 待确认 → 已完成中间穿插已取消。买家下单时展示商品信息、卖家信息、付款金额点击“确认下单”后订单生成并进入待付款状态付款后订单进入待确认卖家在“我的卖出”里看到待确认订单点击确认完成按钮订单关闭买家如果暂不付款可以取消订单。前端用状态字符串来标识每个阶段但不要直接拿这个字符串去显示而是通过计算属性映射成文本和标签颜色。比如const statusMap { pending_payment: { text: 待付款, type: warning }, pending_confirm: { text: 待确认, type: primary }, completed: { text: 已完成, type: success }, cancelled: { text: 已取消, type: info } }低代码项目里很多人喜欢把状态写成数字0、1、2但这个可读性太差前后端调试的时候满屏数字很容易看错。字符串状态是接口层的约定前端做映射表后端的枚举类对应同一组字符串。这样一个后端开发者和前端开发者聊天的时候说的是同一个词而不是去对“2的含义是什么”。订单列表要分“我买到的”和“我卖出的”两个Tab分别调用不同的接口展示不同的操作按钮。我买到的里待付款可以取消或去付款我卖出的里待确认可以确认完成。交易完成后双方可以互相评价这套机制也可以做但只做最基础的评分不引入长文本评价避免整个系统复杂度失控。3.5 后台数据统计与审核管理后台管理模块是校园系统里容易被低估的部分。很多同学把后台当成几个表格页面随便写写结果答辩时被问一句“后台的审核流程怎么设计”就哑了。前台的每一件上架商品后台都要能下架每一个注册用户后台都要能禁用每一条订单后台都要能查到详情。这是交易系统的安全底线。统计部分我推荐加一个图表页用ECharts或者轻量一点的图表库展示近一周发布量、成交量、分类占比。不用做得特别高级两张图足矣。图表数据通过一个统计接口返回后端用分组查询就能搞定。这个页面在答辩演示时很加印象分能让评审老师一眼看到系统收集了数据并且做了可视化展示而不只是增删改查。后台前端我会复用一套同款布局左侧菜单商品管理、订单管理、用户管理、数据统计。权限上用简单的角色字段来判断user表的role字段区分普通用户和管理员前端通过Vue Router的meta字段做动态路由或按钮级权限控制。4. 工程化配置与前后端联调细节4.1 Vite配置与开发环境代理创建项目时我习惯直接用npm create vitelatest选择vue模板比手动配置webpack省太多事。生成后第一个要改的就是vite.config.js这里有两个核心配置alias路径别名和开发环境代理。// vite.config.js import { defineConfig } from vite import vue from vitejs/plugin-vue import path from path export default defineConfig({ plugins: [vue()], resolve: { alias: { : path.resolve(__dirname, src) } }, server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }, build: { chunkSizeWarningLimit: 2000, rollupOptions: { output: { manualChunks: { vendor: [vue, vue-router, pinia, axios], element: [element-plus] } } } } })代理配置务必在写接口前就搞定否则前后端联调时每个请求都要跨域调试浪费时间不说还会让你误以为CORS问题很简单。前端请求地址统一写成/api开头代理把/api转发到后端服务端口生产环境上线时再让Nginx做同样的跳转。开发环境手写完整地址会造成大量修改不要偷懒。上面这段配置里我还加了构建时的manualChunks分包把第三方库单独拆出来这也是性能优化里比较关键的一步。4.2 Axios二次封装与拦截器Axios在项目里最好封装成一个统一的实例而不是在组件里直接用axios.post。为什么因为统一封装能管好token注入、错误提示和加载状态三件事。我的封装思路如下// utils/request.js import axios from axios import { ElMessage } from element-plus const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const userStore useUserStore() if (userStore.token) { config.headers.Authorization Bearer ${userStore.token} } return config }) request.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.msg || 请求失败) return Promise.reject(new Error(res.msg)) } return res.data }, error { if (error.response?.status 401) { // 登录失效跳转登录页 } ElMessage.error(error.message || 网络异常) return Promise.reject(error) } )拦截器里最重要的设计是响应拦截器统一解包。后端返回的JSON格式统一为{ code: 200, msg: success, data: {...} }拦截器里判断code非200就弹错误提示正常就直接返回data字段。这样前端业务代码拿到的是干净的data不需要每调用一个接口都重复判断一次code和弹错误。Token过期处理也是个实际点。响应拦截器收到401时清掉本地token跳转登录页。这个逻辑一定要写不然系统上线后用户停留久了会出现“页面还在操作时报错”的尴尬场景。4.3 路由懒加载与打包部署路由懒加载是Vue3项目里默认的操作Vue Router的component参数直接写成箭头函数动态importconst routes [ { path: /, component: () import(/layout/Index.vue), children: [ { path: , name: Home, component: () import(/views/home/Home.vue) } ] } ]不要小看这个细节不写懒加载的话首屏会把所有页面组件全都打包成一个巨大的JS文件加载速度感人。加上懒加载后每个页面单独成文件浏览器只下载首屏需要的部分体验差距极其明显。部署时托管到Nginx是最常见的方案Vue Router要记得用history模式那么Nginx需要配置try_files。一个最简配置如下server { listen 80; server_name your-domain.com; root /var/www/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api { proxy_pass http://127.0.0.1:8080; } }这个配置解决了两个问题浏览器直接访问子路径时不会404开发期的/api前缀搬到服务器上也同样正确转发到后端服务。跨域不再需要后端同学去配CORS生产环境也不会留下跨域隐患。5. 常见问题与排查技巧实录5.1 响应式数据离职数据和视图不同步这类问题我在给人看项目时遇到最多现象是接口返回数据后页面上没有渲染出列表控制台一查数据其实已经有了。十有八九是reactive的“整体赋值”问题。一个典型的错误写法是const list reactive([]) list res.data // 直接替换整个数组响应式丢失正确的做法是用ref声明列表或者用list.splice(0, list.length, ...res.data)这样更新数组内容。在我的项目里商品列表我统一用ref来管理简单直接不容易踩坑。其他容易翻车的地方还包括直接给reactive对象动态加属性又会涉及新增属性的响应式问题这个在Vue3里已经解决了但如果你的代码还保持着Vue2时代的防御式写法反而会绕晕了自己。5.2 组件渲染异常v-for中的key要用正确值还有一个高频报错场景是控制台警告“Duplicate keys detected”根源是列表的key用的是index。在商品列表含搜索排序时用index做key会造成严重问题列表项状态复用错乱比如一个商品的“收藏”状态串到了另一行。固定商品ID是天然的唯一key但注意不要用随机数或时间戳否则刷新后key漂移组件状态全部重置。Element Plus的表格组件也一样涉及el-table-column时最好在后端返回的数据里保证有一个稳定字段当key。做订单列表时我吃过这个亏当时用订单创建时间当key结果用户在两秒内下了两单页面渲染乱套了。换成订单ID问题立刻消失。5.3 接口请求失败跨域和代理要分清很多初学者搞不清“跨域”和“代理”的区别调试前端页面时看到Network面板里一个红色失败请求就大喊“跨域了”其实未必。开Vite开发服务器后浏览器请求的是5173端口的页面但接口目标是8080这时候如果axios直接发8080请求跨域确实存在但我配置了proxy之后前端请求相对路径/api/goodsVite开发服务器会自动转发到8080浏览器里看到的是同源请求根本不涉及跨域。如果配好代理后接口还是失败优先看Vite控制台有没有报代理错误或者后端是否在正确端口上跑起来。生产模式下Nginx反向代理可能配置了限制请求体大小图片上传失败时优先看nginx的client_max_body_size配置默认1M会让多图上传直接挂掉。5.4 构建部署后页面空白本地开发一切正常npm run build后扔到服务器打开一片空白这个场景我见了不下五次。基本三个原因路由模式导致刷新404、base路径不对、静态资源请求路径带不了前缀。把Vue Router改成history模式再做Nginx的try_files绝大多数404问题能解决如果你部署在子路径下Vite的base配置要改成子路径名称然后资源路径才会正确拼接。如果你把dist直接放在服务器根目录那保持默认的base就完全OK。Element Plus按需引入时如果build后样式丢失检查是否装了unplugin-auto-import和unplugin-vue-components并且vite.config.js的plugins里正确注册了这两个插件。这个配置细节非常容易漏忘了配就会出现“功能正常样式全丢”的诡异现象。6. 优化扩展与个人经验总结到这里一个基于Vue3的校园二手商品交易系统的核心内容已经全部跑通了。从需求拆解、技术选型、前端架构到关键模块实现和联调部署再到常见问题的排查整个过程里我最深的体会是前端技术栈的新旧其实不是项目成败的关键你对业务逻辑的理解深度和代码组织能力才是。后续想在这个基础上继续扩展有两条路很值得做。一是接入真正的支付功能把虚拟的“确认付款”换成微信或支付宝的扫码支付订单状态机和回调处理逻辑会复杂不少但对系统完整性是质变。二是加上消息私信模块让买卖双方可以在站内沟通交易时间和地点这对校园场景的体验提升非常明显。技术上都是在现有架构上加模块不会伤筋动骨。最后分享一个小技巧把所有的业务状态码、接口返回码、错误提示文案都集中定义在一个常量文件里前后端共同维护一份。这个习惯在开发中期联调时能帮你节约大量沟通成本。我在这个项目里把订单状态、商品成色、用户角色都做成了前端常量映射表后端的枚举值和它一一对应排查问题时只要对着常量表看定位速度翻倍。如果你正在做类似的项目建议先按照本文的架构把骨架搭起来跑通主流程之后再做细节打磨。遇到“页面不更新”、“接口跨域”、“打包空白”这类问题回头看看第5节大概率能找到答案。
返回列表