ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Vue3二手车交易平台:前后端分离与接口联调实践

Vue3二手车交易平台:前后端分离与接口联调实践 简介一份基于Vue框架打造的二手车交易平台前端源码面向具备Vue基础的前端开发者、全栈学习者以及希望快速搭建二手车展示与后台管理系统的团队。项目以组件化开发为核心包含车辆展示、搜索过滤、用户登录、消息推送等典型业务模块配套JavaScript脚本处理后端交互与表单验证并设有carvue与caradminvue前后台目录结构清晰便于二次开发。压缩包共166个文件以50个Vue组件、45个JS脚本为主辅以PNG/JPG图标、字体、HTML/CSS及JSON配置等资源整体约13.22MB。压缩包内附带readme说明文档可帮助理解安装与运行流程目前已有301人学习适合用于毕业设计参考、前端实训或真实交易平台原型搭建。1. 为什么二手车交易平台要拿 Vue 来搭前后端分离骨架一辆车从车商拍照上架到用户完成预约中间真正决定开发工作量的不是详情页做得有多炫而是筛选条件、车源状态、询价单这三条数据链路能不能对齐。基于Vue框架的二手车交易平台设计源码在行业内通常指一类以 Vue 3 为主、配合 Vue Router、Pinia、axios 组装出来的完整前端工程管理员维护车源用户按条件检索、查看详情并提交询价前后端通过接口分离。这个题材非常适合作为毕设、简历项目也能作为小团队交付的第一版骨架直接二次开发。如果你已经有 Vue 基础可以直接跳到第 4 章看接口联调如果还在入门阶段按第 2 章的顺序把 mock 数据换成真实接口整条业务链路也能独立走通。2. 二手车交易平台的车源数据模型与 mock 接口设计二手车筛选场景里用户习惯多条件组合品牌、车系、价格区间、里程、年份、排放标准。字段设计不合理页面写得再顺也会在后端联调时返工。这一章先定数据模型再给接口契约和 mock 数据最后处理枚举字典。2.1 车辆主表字段怎么拆才能撑住多条件筛选先看一张能满足二手交易业务常见动作的主表结构字段按「展示、筛选、状态控制」三个用途来分字段名类型说明主要用途idbigint车辆唯一标识详情页、订单外键brandvarchar(32)品牌品牌树筛选seriesvarchar(64)车系车系级联model_yearsmallint上牌年份年份区间过滤mileagedecimal(6,1)表显里程单位万公里里程区间过滤pricedecimal(10,1)售价单位万元价格区间过滤gearboxvarchar(16)变速箱类型下拉筛选energy_typevarchar(16)能源类型下拉筛选emission_stdvarchar(16)排放标准城市落籍判断colorvarchar(16)外观颜色列表展示statustinyint0草稿/1在售/2已售/3下架列表权限控制inspection_reportjson检测报告数据详情页展示created_atdatetime上架时间默认排序几个容易踩的设计细节。价格和里程用decimal而不是int否则按 0.5 万公里、14.8 万元这类真实成交数据做区间查询时会产生浮点误差检测报告用json字段存放漆面、结构、机械三项分级查询时不参与 where 条件读取时一次性取出拆成多张子表反而增加联表成本。status字段是关键的状态机已售车辆不能从列表消失后让用户通过收藏夹链接直接购买后端接口要在查询条件里强制带上状态校验前端也要在详情页对这个字段做兜底判断。2.2 用 mock 数据把车辆列表接口先跑通拿到没有后端的源码时最常见的错误是直接在每个页面组件里放一段假数据。更好的做法是先定义接口函数再把它暂时指向 mock 数据源后期只改一行配置。// src/api/vehicle.js // 前端只依赖这个函数前后端字段对齐后无需修改组件代码 import request from /utils/request export function getVehicleList(params) { return request({ url: /api/vehicles, method: get, params, // GET 请求的筛选条件全部放在 query 上 }) }这个接口函数约定的参数包括page和pageSize控制分页sort控制排序字段为 created_at 或 pricebrand、priceMin、priceMax、mileageMax等筛选字段只在传值时参与请求。接口层和视图层解耦后mock 阶段就可以单独维护一份数据文件。// src/mock/vehicles.js export const MOCK_VEHICLES [ { id: 1001, brand: 大众, series: 迈腾 380TSI, modelYear: 2021, gearbox: auto, mileage: 3.4, price: 15.2, energyType: petrol, emissionStd: n6, color: 极地白, status: 1, inspectionReport: { paint: B, structure: A, mechanical: A }, }, ]配套的列表逻辑放在组合式函数里组件只负责调用// src/composables/useVehicleList.js import { ref } from vue import { MOCK_VEHICLES } from /mock/vehicles export function useVehicleList() { const loading ref(false) const list ref([]) const query async (filters) { loading.value true // 模拟接口耗时联调时把这段替换成 getVehicleList(filters) await new Promise((resolve) setTimeout(resolve, 120)) list.value MOCK_VEHICLES.filter((item) pass(item, filters)) loading.value false } return { loading, list, query } } function pass(item, filters) { if (filters.brand item.brand ! filters.brand) return false if (filters.priceMax item.price filters.priceMax) return false if (filters.mileageMax item.mileage filters.mileageMax) return false return true }pass函数里的三个判断对应筛选的叠加逻辑注意filters.priceMax用而不是||当价格为 0 时不会进入判断如果价格最小值设为 0接口处要做一次空值转换否则会过滤掉所有价格大于 0 的车源。真实接口返回的数据结构通常还会带total和page届时在组合式函数里补上pageInfo对象即可。2.3 枚举字典车况等级和能源类型的前后端约定平台最常用的几组枚举需要在一开始就统一否则前端显示「汽油」后端存的是petrol筛选条件对不上。行业内常见做法是前端维护一张映射表同时让后端接口返回的原始值保持简短语义枚举项存储值界面展示能源类型petrol / hybrid / ev汽油 / 混动 / 纯电排放标准n5 / n6国五 / 国六变速箱manual / auto / dct手动 / 自动 / 双离合车况等级A / B / C优秀 / 正常 / 修整存储值不直接写中文主要考虑到 URL 参数拼接时中文字符需要编码以及多端展示时文案可能调整。车况等级单独说明一下检测报告里的paint、structure、mechanical三项都按 A/B/C 分级列表页展示综合等级详情页展示明细因此前端需要一个getOverallGrade(report)函数取三项最低值作为综合评级这是业务上的明确规则不能由前端自行排序。3. Vue 页面层实现车辆筛选、详情参数与询价表单数据模型定了之后页面层主要解决三个问题筛选条件如何和路由联动、详情页参数如何组织、询价表单怎么校验才能避免垃圾询价单。3.1 列表页筛选防抖、多选与 Vue 路由参数同步筛选条件的难点在于用户操作频率高、条件组合多样。品牌树选中一个品牌后还要追加价格区间、里程上限每次变化都发请求会打爆接口。解决方案是条件统一收敛进一个响应式对象通过watch触发带防抖的查询同时把关键条件同步进路由参数这样刷新页面和分享链接后页面状态不丢。script setup import { watch, ref } from vue import { useRoute, useRouter } from vue-router import { useVehicleList } from /composables/useVehicleList const route useRoute() const router useRouter() const { list, loading, query } useVehicleList() const filters ref({ brand: route.query.brand || , priceMax: Number(route.query.priceMax || 0), priceMin: Number(route.query.priceMin || 0), }) function updateFilters(key, value) { filters.value[key] value router.replace({ query: { ...route.query, [key]: value } }) } watch(filters, async (val) { const timer setTimeout(() query(val), 300) return () clearTimeout(timer) }, { deep: true }) /script参数说明updateFilters里先把值写入filters再由 watch 统一触发请求避免每个控件各自请求一次router.replace不产生历史记录用户按浏览器返回时不会陷入筛选条件堆栈。route.query里的值都是字符串初始赋值必须转成 Number否则priceMax 0的判断会把字符串0当成真值。防抖 300 毫秒适合列表请求如果后续接入实时车辆推荐改成 150 毫秒会更跟手。3.2 详情页参数表、检测报告与图片预览的取舍详情页的车辆参数表是传递信任感的核心区域布局上建议用两列描述列表避免单列过长导致关键参数落在首屏之外。这里不需要重新发明组件直接基于 Vue 的插槽机制把字段映射表渲染出来。template el-descriptions :column2 border el-descriptions-item label表显里程{{ detail.mileage }} 万公里/el-descriptions-item el-descriptions-item label上牌年份{{ detail.modelYear }}/el-descriptions-item el-descriptions-item label变速箱{{ gearboxMap[detail.gearbox] }}/el-descriptions-item el-descriptions-item label排放标准{{ emissionMap[detail.emissionStd] }}/el-descriptions-item /el-descriptions /template图片预览部分不建议本来只用 8 到 10 张图就接入重型图片库。常见做法是手写一个 30 行的PhotoPreview组件点击缩略图时打开全屏遮罩左右按钮切换当前索引同时把图片长按保存禁用掉。实现时需要留意循环绑定的索引更新时机使用currentIndex (currentIndex 1) % images.length处理末尾回卷并在遮罩层监听keydown事件绑定左右方向键。这套轻量方案在图片数量有限的场景下比引入通用画廊组件少加载约 200KB 依赖。3.3 询价表单校验什么时候提示、怎么避免无效询价用户愿意填询价单说明这辆车已经过了初筛这时校验策略应该偏宽松只拦截明显无效的数据而不是用十几条规则劝退用户。// src/views/vehicle/inquiryForm.js const form reactive({ username: , phone: , expectedPrice: , note: , }) function validate() { if (!form.username.trim()) return 请填写称呼 if (!/^1[3-9]\d{9}$/.test(form.phone)) return 手机号格式不正确 if (Number(form.expectedPrice) 1) return 期望价格不能为空 return }正则^1[3-9]\d{9}$只做基本格式校验不判断号段归属因为二手车的客户可能用携号转网后的非常规号段expectedPrice用 Number 转数值后判断下限避免用户填入 0 或负数。校验时机选择失焦后触发而不是实时触发只在用户提交时最后做一次全量校验。错误展示不要把alert弹窗拍在用户脸上把错误文本渲染到对应表单项下方并让第一个出错字段自动获得焦点这样用户能快速定位。4. Pinia 与 axios 联调把车源工程接上真实后端mock 阶段跑通页面后接真实后端是源码工程能不能落地的关键。常见问题集中在三个位置全局状态边界混乱、axios 错误处理分散、路由守卫和登录态恢复配合不当。后端技术栈和 Vue 无关用 Spring Boot 也好用 Node 也行前端这层统一做好适配。4.1 三个 Pinia 模块划定二手车交易平台的状态边界交易平台的状态不要全塞进一个 store按业务域拆成用户、车辆、订单三个模块模块之间不互相引用对方内部可变字段。// src/stores/useUserStore.js import { defineStore } from pinia export const useUserStore defineStore(user, { state: () ({ token: localStorage.getItem(token) || , profile: null, }), getters: { isLogin: (state) !!state.token, }, actions: { setToken(token) { this.token token localStorage.setItem(token, token) }, logout() { this.token this.profile null localStorage.removeItem(token) }, }, })边界划分原则token和profile属于用户域车辆查询条件属于车辆域询价单草稿属于订单域。这里有个常见误用很多人把「当前选择的车源 ID」也放进用户 store导致退出登录后车辆页状态被清空。正确做法是把这类能由路由参数推导的状态放在组件或独立的车辆 store 里。Pinia 的getters可以用来派生isLogin尽量不要在每个组件里重复写!!store.token的判断。4.2 axios 实例封装与错误码映射表后端接口返回的 HTTP 状态码和业务错误码是两回事。前端要统一处理的是业务侧语义先把常见码位整理成一张映射表错误码含义用户看到的现象统一处理方案401未登录或 token 过期点击按钮无响应清除 token跳转登录页并记录回跳路径403无权限访问该车源列表闪烁后消失提示权限不足返回列表页404车源已下架详情页空白轻提示后回列表页500服务端异常接口一直 loading关闭 loading展示可重试文案配合这张表axios 实例封装在拦截器里做收敛。注意拦截器里拿不到全局路由实例时会报错需要先 import 路由对象再使用。// src/utils/request.js import axios from axios import { ElMessage } from element-plus import { useUserStore } from /stores/useUserStore import router from /router const service axios.create({ baseURL: import.meta.env.VITE_API_BASEURL || /api, timeout: 8000, }) service.interceptors.request.use((config) { const store useUserStore() if (store.token) { config.headers.Authorization Bearer ${store.token} } return config }) service.interceptors.response.use( (response) response.data, (error) { const status error.response?.status if (status 401) { const store useUserStore() store.logout() router.push({ path: /login, query: { redirect: router.currentRoute.value.fullPath } }) } ElMessage.error(error.response?.data?.message || 请求失败) return Promise.reject(error) } )参数说明timeout设 8 秒二手车详情页图片接口慢于这个时间就主动断开避免页面长时间停在 loading 状态Authorization头使用Bearer前缀需要前后端约定一致后端过滤器认不到这个前缀时会统一报 401。响应拦截器直接返回response.data让业务代码拿到的是数据体而非 axios 包装结构。注意logout()里不要顺手调用跳转登录页的跳转统一放在 401 分支里避免每个请求错误时重复触发路由。4.3 路由守卫与登录态恢复的常见坑交易平台里询价、收藏、后台管理页面需要登录态路由守卫要完成两件事未登录用户跳转登录页已登录用户刷新后恢复用户信息。// src/router/index.js router.beforeEach(async (to) { const store useUserStore() if (to.meta.auth !store.token) { return { path: /login, query: { redirect: to.fullPath } } } if (store.token !store.profile) { try { await store.fetchProfile() } catch (e) { store.logout() return { path: /login } } } return true })这个写法解决了一个常见 bug刷新页面后 token 还在 localStorage 里但profile变成了null如果不做「有 token 无 profile 就去拉取用户信息」的判断进入后台时界面会短暂显示未登录状态。redirect参数的回跳逻辑要在登录页提交成功后处理用router.replace(route.query.redirect || /)而不是直接router.push(/)。另外注意beforeEach内部不能调用自身跳转否则在 token 刷新后重新导航会形成死循环。5. 构建优化与上线前的性能验证技巧二手车平台的图片多、列表长打包产物很容易超过 1MB。上线前最值得花时间的三件事路由懒加载、列表图片懒加载、静态资源缓存配置。5.1 路由懒加载与首屏指标验证把所有页面组件改成按需加载是成本最低的优化手段。Vue Router 4 语法如下// src/router/index.js // 修改前同步 import // import VehicleDetail from /views/vehicle/Detail.vue // 修改后路由懒加载进入页面时才下载对应 chunk const VehicleDetail () import(/views/vehicle/Detail.vue)验证懒加载是否生效打开浏览器 Network 面板刷新首屏后观察是否只有一个页面对应的 chunk 文件被加载再点击详情页路由看是否有新增 js 文件请求。如果首屏仍然加载了所有页面的打包文件检查路由表里是否混有同步 import 的组件。5.2 图片懒加载与虚拟列表的取舍车辆列表页一般有大量缩略图用浏览器原生的 IntersectionObserver 做懒加载不需要引入第三方指令库。// src/directives/lazy.js // 使用方式img v-lazyitem.cover / const lazy { mounted(el, binding) { const observer new IntersectionObserver((entries) { entries.forEach((entry) { if (entry.isIntersecting) { el.src binding.value observer.disconnect() } }) }) observer.observe(el) }, }这个指令只在图片进入视口时才赋值src加载完成后立即disconnect释放观察器。注意不要给img预先写死src属性否则浏览器会在指令执行前就开始下载图片懒加载失去意义。列表数量超过 500 且行高固定时才值得引入虚拟列表二手车列表页一般在 200 条以内强上虚拟滚动反而造成滚动条跳动和焦点管理问题。5.3 静态资源缓存与 nginx 配置打包后的资源文件带 hash 指纹可以放心长缓存入口 index.html 必须走协商缓存这样发版后用户能拿到新资源。server { listen 80; root /var/www/dist; location /assets/ { expires 30d; add_header Cache-Control public, immutable; } location / { try_files $uri $uri/ /index.html; } }nginx 配置里immutable告诉浏览器该文件内容永不变化因为文件名里的 hash 变了 URL 也会变浏览器会重新请求新文件。try_files最后指向/index.html解决了 Vue Router 的 history 模式刷新页面时 404 的问题但如果项目部署在子路径下base配置和这里location /assets/的路径需要同步修改。提示Vue 项目打包后布局异常多半是base路径设置不对。检查vite.config.js里的base字段确认 dev 环境的/路径在部署后变成子路径时nginx 的root指令和 assets 路径是否仍然匹配。本文还有配套的精品资源点击获取
返回列表