简介:基于Vue框架的家政服务平台用户端设计源码,是一套完整的前端工程,面向正在学习组件化开发、单页应用搭建或家政类业务界面的开发者。压缩包共194个文件、约2.6MB,包含65个Vue组件、50个JavaScript脚本、53张PNG图片,以及JSON配置、SCSS/CSS样式、HTML模板和Markdown说明等,分别承担页面结构、交互逻辑、视觉素材、项目配置与文档说明,目录清晰,便于按模块查阅。项目实践了Vue-router路由管理、Vuex状态管理、Element UI组件库和TypeScript类型增强,并采用合理的模块划分与命名规范,整体体现出高内聚、低耦合的现代前端架构思路。已有85人学习浏览,适合想了解家政服务用户端完整实现、准备相关课程设计或需要参考工程化项目组织的读者,可直接对照源码梳理路由、状态、组件与样式之间的协作方式。
1. 基于Vue框架的家政服务平台用户端设计源码,先解决“能交付”的问题
家政服务用户端不是把服务列表和下单按钮堆在页面上,用户从打开小程序或 H5 到付完款,会经历选分类、选服务者、选时间、填地址、确认订单、查进度、评价这一整条链路。标题里的“设计源码”,指的是一份能跑、能打包、能接真实接口的 Vue 工程。Vue 胜任这类表单多、页面跳转频繁、订单状态复杂的 C 端项目,开发速度比原生快,社区里成熟的移动端组件库能覆盖大部分交互。这篇按一线人员做用户端前端的方式,把骨架搭建、预约下单、移动端适配和验证工具拆开讲,适合正在做类似前台的新人,也方便老人回头检查自己的项目边界。
2. 用户端项目骨架:路由分层、状态管理和API封装
用户端项目最容易失控的地方不是页面多,而是数据在页面之间传来传去、接口报错时逻辑散落在业务文件里。拿到源码先别急着写视觉,把骨架立住:Vue 版本和构建工具、路由的层级、全局状态该放什么、请求层如何统一处理 token 和错误码。这四件事定了,后续每个业务页只需要专注自己的局部状态。
2.1 用 Vite 搭 Vue3:vue安装及环境配置到这步就够了
常见做法是 Vue3 + Vite。Vue2 已经停止维护,新项目没必要选老版本;Vite 基于 esbuild 做预构建,冷启动秒开,开发体验比 vue-cli webpack 好不少。vue安装及环境配置就是三行命令的事,不必再手动引入构建配置。
node -v npm create vite@latest home-user -- --template vue cd home-user npm install npm install vue-router@4 pinia axios npm run dev说明:--template vue生成的是 Vue3 单页应用模板;vue-router@4是 Vue3 配套路由版本;pinia用来存跨页面的用户和订单草稿;axios统一走请求封装。Node 版本建议 18 以上,16 也能跑但新版 Vite 有时要额外加配置。
工程目录我一般按下表拆,避免业务代码堆在一起:
| 目录 | 职责 |
|---|---|
| src/api | 按模块拆接口函数,只做请求描述 |
| src/router | 路由表、路由守卫 |
| src/stores | Pinia 状态模块 |
| src/views | 页面级组件,一个目录一个业务 |
| src/components | 公共组件,不掺接口逻辑 |
| src/utils | 请求实例、时间格式化、校验函数 |
这样拆之后,views里尽量不要出现axios的直接调用。业务组件只认api层导出的函数,后续后端改字段时只改一处。
2.2 按页面纬度拆路由,vue路由参数的三种收法
用户端路由不能做成一长串平铺列表。我一般分三层:带底部 tab 的首页框架层、需要登录的业务层、登录注册等隔离层。路由表写在src/router/index.js:
const routes = [ { path: '/', component: () => import('@/layouts/UserTabLayout.vue'), children: [ { path: '', name: 'Home', component: () => import('@/views/home/HomeView.vue'), meta: { keepAlive: true } }, { path: 'orders', name: 'OrderList', component: () => import('@/views/order/OrderListView.vue') } ] }, { path: '/order/:orderId', name: 'OrderDetail', component: () => import('@/views/order/OrderDetailView.vue') }, { path: '/login', name: 'Login', component: () => import('@/views/login/LoginView.vue') } ]这里有一个 vue路由参数 的关键点:/order/:orderId这种路径参数、query参数、props传参是三种不同收法。路径参数适合订单详情这种必须存在的 ID;query参数适合?from=orderList这种可选来源;props传参适合把orderId解耦成组件的正常 prop,方便单元测试。用户端订单详情页常需要从列表页、支付回调、消息通知三个入口跳进来,统一用路由名加params跳转更省心:
router.push({ name: 'OrderDetail', params: { orderId: '20250201001' } })2.3 Pinia放什么:订单草稿、定位信息和用户会话
用户端跨页面的数据只有几类:用户 token 和资料、当前定位、正在编辑的订单草稿。把这些放 Pinia,其余能用组件 props 传递的状态不要塞进来,否则刷新一次全丢,反而更难排查。订单草稿适合放 Pinia,因为用户可能从首页选服务后去改地址,再回订单确认页,中间经历多次路由切换。
import { defineStore } from 'pinia' export const useOrderDraftStore = defineStore('orderDraft', { state: () => ({ categoryId: null, serviceTime: '', addressId: null, workerId: null, remark: '' }), actions: { setDraft(partial) { this.$patch(partial) }, clearDraft() { this.$reset() } } })注意setDraft用$patch合并更新,不是整体覆盖;用户在日历页改了时间,回上一页改地址,不会互相清空。clearDraft要在提交订单成功或用户取消创建时调用,不然下一次下单会带着上次残留的 workerId。刷新页面后 Pinia 数据会消失,所以订单草稿只用于下单流程内;真正已生成的订单必须从接口拉详情,不要在前端拼。
2.4 axios实例封装:token注入、401跳转和重复提交拦截
所有接口统一走一个 axios 实例,业务文件不要再axios.get。请求层要做的事包括:自动带 token、按后端code字段解包、token 失效时跳登录、网络超时给统一提示。
import axios from 'axios' const service = axios.create({ baseURL: import.meta.env.VITE_API_BASE, timeout: 10000 }) service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) service.interceptors.response.use( res => { const { code, data, msg } = res.data if (code === 0) return data if (code === 401) { localStorage.removeItem('token') window.location.href = '/login?redirect=' + encodeURIComponent(location.pathname) return Promise.reject(new Error('登录已过期')) } return Promise.reject(new Error(msg || '请求失败')) }, err => { if (err.code === 'ECONNABORTED') { return Promise.reject(new Error('请求超时,请检查网络')) } return Promise.reject(err) } )开发环境里VITE_API_BASE通常配成/api,由 Vite dev server 把/api转发到后端地址,这样不产生跨域问题。响应拦截器里code的判定取决于后端约定,有的是code: 200,有的是undefined,统一在 api 层确认最合适。下单按钮的重复提交不要全依赖按钮disabled,最好在提交函数里加一个模块级锁,防止用户连点两次触发两条订单。
let submitting = false async function submitOrder(payload) { if (submitting) return submitting = true try { await placeOrder(payload) } finally { submitting = false } }submitting这个变量要放在组件外部或独立模块里,放在reactive里也能用,但要注意组件卸载时锁没有释放,可能影响下一次进入页面。
3. 预约下单链路:服务分类、日历排期与订单状态机
家政用户端最核心的转化动作是预约下单。和其它电商不同,家政订单强依赖服务时间和地点,用户选错时间、地址后取消率极高。这一章把下单前的分类筛选、日历排期、表单校验以及提交后的订单状态流转串起来,源码层面的重点不是“页面好看”,是“用户每次点击都有边界”。
3.1 服务分类的数据模型与筛选面板
家政服务分类常有二级结构:一级是“日常保洁”“家电清洗”“保姆月嫂”等,二级是“日常保洁”“深度保洁”“擦玻璃”等细分服务。接口返回的树形结构直接复用,不要在前端重排层级。数据模型至少需要这些字段:
| 字段 | 说明 |
|---|---|
| id | 分类唯一 ID |
| parentId | 父分类 ID,一级传 0 |
| name | 分类名称 |
| sort | 排序权重,从小到大 |
| icon | 分类图标 URL |
页面里一级分类做成顶部横向滚动,二级做成左侧两列网格。用 computed 筛出当前一级下的二级分类:
const categories = ref([]) const activeFirstId = ref(null) const firstLevel = computed(() => categories.value.filter(c => c.parentId === 0) ) const secondLevel = computed(() => categories.value.filter(c => c.parentId === activeFirstId.value) )筛选面板只解决“选哪个分类”,真正提交订单时必须把分类id传回后端,不能只传一个中文名。用户选完一级分类后,activeFirstId变化会触发secondLevel重新计算,二级列表里的“立即预约”按钮要直接进入下单确认页,并把categoryId写进orderDraft。
3.2 日历排期组件:不可用日期和时段计算
排期模块是家政用户端和普通电商最不同的地方。服务时间由前端传给后端,后端再校验服务者是否可约,所以前端要做的第一步是把不可约的时段置灰,避免用户反复提交失败。
常见做法是后端提供一个可用时段接口,返回未来 7 天内每个日期和时段的剩余可约数量。前端在拿到数据后,按当前时间过滤掉已经过去的时段。例如早上 10 点整,用户选“今天 09:00-11:00”,这个时段必须置灰;选“今天 14:00-16:00”则保留。
function buildTimeSlots(selectedDate, serviceDuration) { const now = new Date() const target = new Date(selectedDate) const slots = [] for (let hour = 8; hour <= 20; hour += serviceDuration) { const slotStart = new Date(target) slotStart.setHours(hour, 0, 0, 0) if (slotStart.getTime() <= now.getTime() + 2 * 60 * 60 * 1000) { continue } slots.push({ start: `${String(hour).padStart(2, '0')}:00`, end: `${String(hour + serviceDuration).padStart(2, '0')}:00` }) } return slots }这里的2 * 60 * 60 * 1000表示至少提前 2 小时预约,具体时间由运营规则决定。按这个函数生成的 slots 是纯前端过滤,后端下单接口仍要再做一次真实校验,因为可能存在两个用户同时抢最后一个时段,前端置灰不能替代接口幂等。
3.3 下单表单校验:地址、联系人、备注和优惠券
用户从日历选完时间后进入订单确认页,页面上要填的常见字段是服务地址、联系人电话、期望服务时间、备注和优惠券码。校验规则不需要引入复杂验证库,轻量函数足够。
function validateOrderForm(form) { const errors = {} if (!form.serviceTime) { errors.serviceTime = '请选择服务时间' } if (!/^1[3-9]\d{9}$/.test(form.contactPhone)) { errors.contactPhone = '手机号格式错误' } if (!form.addressId) { errors.addressId = '请选择服务地址' } if (form.couponCode && form.couponCode.length > 16) { errors.couponCode = '优惠券码过长' } return { valid: Object.keys(errors).length === 0, errors } }手机号正则用于国内手机号基础校验,真正有效性仍以后端短信验证为准。地址这里使用addressId,用户从地址簿选择后把完整地址存进 Pinia,提交时只传addressId,后端根据 ID 取详情;不要在前端拼“省市区+详细地址”这样一个大字符串,容易出现分隔符不统一的问题。优惠券码校验通过后再计算折扣金额,金额计算必须在后端做,前端展示的优惠金额只做视觉反馈。
3.4 订单状态机:用事件表代替if-else
用户提交订单后,订单状态在不同页面会有完全不同的展示:待支付显示倒计时和“立即支付”,已支付等待派单,服务者已接单进入进行中,服务完成可以评价。状态之间的跳转是有限的,不是所有操作在所有状态都合法。不要在一堆if (order.status === 'pending')里写业务判断,用一个事件表管理流转:
| 当前状态 | 允许事件 | 目标状态 |
|---|---|---|
| pending | pay | paid |
| pending | cancel | canceled |
| paid | assign | assigned |
| assigned | start | working |
| working | complete | completed |
| completed | comment | commented |
对应的状态机函数可以写成:
const transitions = { pending: { pay: 'paid', cancel: 'canceled' }, paid: { assign: 'assigned', refund: 'refunding' }, assigned: { start: 'working', cancel: 'canceled' }, working: { complete: 'completed' }, completed: { comment: 'commented' } } function applyTransition(order, event) { const nextStatus = transitions[order.status]?.[event] if (!nextStatus) { throw new Error(`订单状态 ${order.status} 不允许执行 ${event}`) } return { ...order, status: nextStatus } }这样设计以后,新增一个“客服介入”状态只要在transitions表里加一行,不需要去所有页面排查if。用户端拿到订单详情后,页面根据当前状态决定显示哪个按钮组,按钮的点击事件直接调用applyTransition做前端预校验,再请求后端接口。前端校验只是体验优化,真正的状态更新必须以后端返回为准;接口返回新状态后,用新状态重新渲染页面。
4. 移动端适配、登录守卫和打包后布局异常的排查
用户端大部分流量在手机上,布局适配和登录态是源码交付前最容易出问题的两个环节。Vue 开发时一切正常,打包上线后却出现白屏、图标错位、页面刷新 404,这些问题几乎都在第 4 章涵盖的范围之内。
4.1 移动端适配:vw是用户端的最省心选择
适配方案不需要纠结太久。新项目一律走 vw 方案,设计稿按 375 宽度出图时,vite 配置里把viewportWidth设成 375,源码里继续写 px,构建时自动转成 vw。没必要再引入 rem 动态设置根字号那套方案,vw 更少 JS 参与,也不受浏览器默认字号影响。
| 方案 | 优点 | 缺点 |
|---|---|---|
| vw | 纯 CSS 转换,无 JS 运行时代价 | 个别安卓 WebView 对高精度 vw 支持一般 |
| rem | 老项目兼容好 | 需要动态设置 html font-size |
| px 硬写 | 最直观 | 不同屏幕差异明显 |
Vite 中接入 vw 转换,常用postcss-px-to-viewport-8-plugin:
import postcssPxToViewport from 'postcss-px-to-viewport-8-plugin' export default defineConfig({ css: { postcss: { plugins: [ postcssPxToViewport({ viewportWidth: 375, unitPrecision: 5, minPixelValue: 1, selectorBlackList: ['.ignore-'] }) ] } } })unitPrecision控制转换后小数的保留位数;minPixelValue: 1表示 1px 以下的边框不转换;selectorBlackList里放不需要转换的类名,比如第三方日历组件的样式,避免组件内部被干扰。设计稿如果是 750 宽度,viewportWidth设 750 即可。字体大小建议不要全盘转 vw,正文和按钮用固定像素或响应式字号混合处理,避免超大屏上文字糊成一团。
4.2 用 vue路由守卫把登录和注册系统串起来
用户端必然有登录注册,但不该在每个页面里判断 token。用路由表的meta.auth标记哪些页面需要登录,然后在全局守卫里统一拦截。
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.meta.auth && !token) { next({ path: '/login', query: { redirect: to.fullPath } }) } else { next() } })登录成功后,用redirect参数跳回用户原来想访问的页面:
const redirect = route.query.redirect || '/' router.replace(redirect)注意meta.auth不只写在路由对象上,嵌套路由的子路由也需要单独标注。用户收藏列表、订单列表、个人资料编辑这些页面都要加auth: true,首页、服务列表、登录注册页保持公开。token 存放在localStorage里,登录态刷新后仍存在;如果安全要求高,可改用 httpOnly cookie 配合后端 session,但前端路由守卫的判断逻辑不变。
4.3 vue打包后布局异常的排查路径
“vue 打包后 布局异常”是一个高频检索词,实际原因通常集中在三个地方:静态资源路径、路由 history 刷新 404、CSS 被压缩后顺序变化。
静态资源路径在vite.config.js里配置base。项目部署在域名根目录时用默认/,部署在子路径时改成子路径:
export default defineConfig({ base: process.env.NODE_ENV === 'production' ? '/home/user/' : '/' })路由如果是createWebHistory(),后端必须把所有非接口路径都返回index.html。Nginx 示例:
location / { try_files $uri $uri/ /index.html; }没有这段配置,用户在订单详情页直接刷新页面,Web 服务器找不到/order/xxx这个真实文件,返回 404,看起来就像“打包后布局异常”。如果后端不方便做try_files,可以改成createWebHashHistory(),代价是 URL 带#,但对部署最简单,几乎不会 404。
CSS 错位问题优先检查scoped是否误伤第三方组件。scoped会给选择器加><div v-for="worker in workerList" :key="worker.id" v-memo="[worker.price, worker.rating, worker.favorite]" class="worker-card" > <span>{{ worker.name }}</span> <span>{{ worker.price }}元/小时</span> <span>{{ worker.rating }}分</span> </div>
v-memo的数组值在上一次渲染后没有变化,组件就会复用旧 vnode,跳过 diff。这里是按[worker.price, worker.rating, worker.favorite]作为依赖,意思是列表项内部用到哪些字段,就把哪些字段列进去。如果列表项内部用了当前时间或随机数,v-memo反而产生性能问题;不确定依赖时宁可不写,先测数据量再决定是否优化。配合路由的meta.keepAlive使用,用户从服务者列表切到详情页再返回,列表滚动位置能保持住。
5. 用 vue devtools 和 vConsole 给用户端源码做最后验证
源码能不能交付,除了功能跑通之外,还要经得住“真机打开看一眼”这一步。这个阶段我一般做三件事:用 vue devtools 检查组件状态、在移动端页面挂 vConsole 看日志、给关键链路加轻量埋点。
5.1 vue devtools插件下载后先干三件事
vue devtools插件下载后不用急着看界面。先在浏览器启动应用,按下 F12 打开 Vue 面板,按顺序做三个检查:
第一,看图层面板里的路由组件是否被KeepAlive正确缓存。用户从首页进入服务列表,再返回首页,首页不应该重新请求分类接口。第二,打开 Pinia 面板,手动把orderDraft里的serviceTime改掉,确认页面上的日历高亮会同步变化,这一步能快速暴露“页面还在用本地 state,没用 store”的问题。第三,在组件树里选中下单按钮,查看点击事件绑定的函数是否来自正确的组件实例,排查事件重复绑定。
这三个检查不用写代码,却能发现静态检查发现不了的运行时问题。
5.2 移动端接入vConsole,别在生产环境暴露
手机浏览器没有控制台,页面报错看不到。vConsole 是轻量方案,引入后在页面右下角出现一个控制台按钮,能看到 console 日志、网络请求和本地存储。
import VConsole from 'vconsole' if (import.meta.env.MODE !== 'production') { const vConsole = new VConsole() vConsole.setOption('maxLogNumber', 300) }import.meta.env.MODE是 Vite 的环境变量,production构建环境下不会创建实例,避免把调试入口带给真实用户。测试环境需要看网络请求时,可以在vite.config.js里定义loadEnv模式,单独出一个 staging 环境允许 vConsole;生产环境的开关不要做成 URL 参数控制,容易被发现后滥用。
5.3 关键节点埋点,复制状态事件表
用户端下单流程的转化率需要数据支撑,至少要埋这几个事件:进入下单页、选择服务时间、选择地址、提交订单成功、提交订单失败、支付成功。埋点函数可以很简单:
const trackEvent = (event, data = {}) => { console.log('[track]', event, data) // 上报到自己的统计平台 } function submitOrderTrack(orderId) { trackEvent('user_submit_order', { orderId, source: 'home_user', ts: Date.now() }) }source: 'home_user'用来区分用户端和员工端,埋点和第 3 章的订单状态事件表保持一致,事件名不要出现中文。最后保留一个最小技巧:在本地放一个mock目录,用vite-plugin-mock按接口路径建同名文件,mock 数据里订单状态、服务时间、地址字段要和真实接口完全一致,切环境时只改env文件里的VITE_API_BASE,这样源码交付后接手的人不用改一行业务代码,就能在不同环境之间切换。
本文还有配套的精品资源,点击获取