Vue3二手车交易平台:前后端分离与接口联调实践
2026/9/16 9:33:35 网站建设 项目流程

简介:一份基于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 上 }) }

这个接口函数约定的参数包括:pagepageSize控制分页,sort控制排序字段为 created_at 或 price,brandpriceMinpriceMaxmileageMax等筛选字段只在传值时参与请求。接口层和视图层解耦后,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 的车源。真实接口返回的数据结构通常还会带totalpage,届时在组合式函数里补上pageInfo对象即可。

2.3 枚举字典:车况等级和能源类型的前后端约定

平台最常用的几组枚举需要在一开始就统一,否则前端显示「汽油」,后端存的是petrol,筛选条件对不上。行业内常见做法是前端维护一张映射表,同时让后端接口返回的原始值保持简短语义:

枚举项存储值界面展示
能源类型petrol / hybrid / ev汽油 / 混动 / 纯电
排放标准n5 / n6国五 / 国六
变速箱manual / auto / dct手动 / 自动 / 双离合
车况等级A / B / C优秀 / 正常 / 修整

存储值不直接写中文,主要考虑到 URL 参数拼接时中文字符需要编码,以及多端展示时文案可能调整。车况等级单独说明一下:检测报告里的paintstructuremechanical三项都按 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 :column="2" 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') }, }, })

边界划分原则:tokenprofile属于用户域,车辆查询条件属于车辆域,询价单草稿属于订单域。这里有个常见误用:很多人把「当前选择的车源 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-lazy="item.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 路径是否仍然匹配。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询