
简介一套基于Vue框架与Java后端技术栈开发的民宿管理系统设计源码面向需要学习前后端分离开发的学生、毕业设计者以及有中小型民宿信息化管理需求的个人或团队。资源压缩包大小12.36MB共239个文件主要包含Java、Vue、TypeScript、SQL及图片素材Java文件实现后端服务与业务逻辑Vue文件构建单页面应用前端TypeScript提升代码类型安全SQL脚本提供数据库建表与初始化数据另有Markdown说明文档辅助理解项目结构。截至目前已有245人学习下载。通过源码可系统掌握民宿管理中房间管理、订单处理、客户信息等核心模块的开发思路学习Vue组件化与Spring Boot后端的整合方式同时完整的前后端分离目录与配置也方便在此基础上改造复用或作为课程设计、毕业设计的参考项目。1. 基于Vue的民宿管理系统设计源码先理顺业务模型再谈页面民宿管理系统的源码和普通后台管理系统最大的区别在于它有一套自己的业务状态机房间在不同日期可以是空闲、预订中、入住中、清洁中订单从待支付到已退房之间存在严格的流转约束。直接用表格 CRUD 的思路去做开发到一半必然被房态冲突和订单状态错乱拖垮。这篇文章围绕“基于Vue的民宿管理系统设计源码”这个主题讲一套可以落地的系统设计方案技术选型、目录组织、路由与状态管理、API 封装与权限控制最后落到部署和源码调试。目标读者是已经会写 Vue 单页应用、想完整负责或接手一套民宿管理系统源码的开发者。2. 民宿管理系统的 Vue 技术选型与工程目录组织一套民宿管理系统落到代码里最先确定的不是页面长什么样而是技术栈和目录结构。我见过不少从脚手架自动生成的项目业务一复杂就出现两个问题所有接口堆在同一个文件里所有页面组件平铺在 views 目录下。要避免这种情况必须在写第一行业务代码前把三件事定下来依赖版本、src 各层的职责边界、路由与状态的挂载方式。2.1 Vue 3 与配套依赖的选型依据技术项推荐选型选型理由前端框架Vue 3.4 Vite 5组合式 API 让房间、订单、财务模块的代码按业务聚合Vite 的冷启动比 Webpack 明显更快路由Vue Router 4与 Vue 3 配套支持嵌套路由和动态添加路由适合后台布局加多级菜单的民宿系统状态管理Pinia相比 Vuex 去掉了 mutation 概念store 按模块拆分调试时直接观察 state 变化UI 组件Element Plus表格、弹窗、日期选择器、步骤条覆盖民宿后台的日常操作场景HTTP 客户端axios拦截器机制成熟统一处理 token 注入、401 跳转和错误消息日期计算dayjs结算入住天数、门限时间等依赖高质量的日期 APIdayjs 体积小且链式调用清晰这套选型是目前 Vue 中后台项目最常见组合。如果你的民宿系统后续要接微信小程序端接口层多包一层就能复用业务逻辑如果只是内部运营后台Element Plus 全量引入反而省事首版迭代阶段不必纠结按需加载的打包体积。2.2 用 Vite 在本地跑起民宿管理系统的最小命令假设从零开始构建Vite 生成的模板比 vue-cli 精简适合自己掌握结构。常用命令如下npm create vitelatest guesthouse-admin -- --template vue cd guesthouse-admin npm install npm install vue-router4 pinia element-plus axios dayjs npm run dev第 1 条命令创建名为guesthouse-admin的 Vue 工程--template vue表示使用官方 Vue 模板。第 4 条补充安装路由、状态管理、UI 库、请求库和日期库这里不写-S是因为 npm 8 之后默认把依赖写入 dependencies。最后npm run dev启动开发服务Vite 默认跑在5173端口如果端口被占用会自动换一个可用端口并在终端打印最终地址不要盯着固定端口不放。开发服务跑起来之后要立刻做一件事把src/main.js里的路由、Pinia、Element Plus 按顺序挂载。常见做法是import { createApp } from vue import { createPinia } from pinia import ElementPlus from element-plus import element-plus/dist/index.css import App from ./App.vue import router from ./router const app createApp(App) app.use(createPinia()) app.use(router) app.use(ElementPlus) app.mount(#app)这里的挂载顺序有讲究先createPinia()再注册路由是为让路由守卫里能通过useUserStore()访问 storeElement Plus 放在最后是因为它只影响组件渲染不依赖业务层。2.3 目录结构怎么划避免后期改不动我一般会按“接口、组件、布局、路由、状态、页面、工具”七层来组织 srcsrc/ api/ modules/ # 按业务模块拆分接口文件 room.api.js order.api.js user.api.js request.js # axios 实例与拦截器 components/ # 公共展示组件 room-slot-card.vue order-status-tag.vue layout/ # 后台布局侧边栏、顶部栏 sidebar-menu.vue router/ routes/ # 静态路由与权限路由分开定义 static.js dynamic.js index.js stores/ # Pinia 按业务模块拆分 room.js order.js user.js views/ room/ calendar.vue # 房态日历 slot.vue # 房间维护 order/ list.vue detail.vue finance/ settlement.vue system/ user.vue utils/ order-status.js # 订单状态机这套划分的核心思路是让业务模块成为主导而不是按页面堆文件。以民宿管理为例room相关的接口调用在api/modules/room.api.js状态维护在stores/room.js页面组件在views/room/下三个位置一一对应。接手的同事即使没看过整体设计也能按模块名快速定位需要改动的地方。components目录只放跨模块复用的展示型组件比如订单状态标签、房间卡片一旦发现某个组件只被一个页面使用就应该把它挪到对应页面目录下避免公共组件库变成垃圾场。3. 用 Vue Router 和 Pinia 实现民宿房态与订单状态流转民宿管理系统里最核心的两条业务线是“房态”和“订单”。房态描述的是某个房间在当前时刻或某一天处于什么状态订单描述的是客人预定的完整生命周期。先设计这两条线的数据流转再去做页面代码组织会顺畅很多。3.1 路由表划分静态路由与权限路由分开定义民宿管理系统的访问者通常有两类前台或店长老板或管理员。前台能看到房态和订单但财务报表和系统设置只有管理员能进。如果所有菜单都写在静态路由表里前端虽然能用 v-if 隐藏菜单用户依然可以手动敲完整 URL 访问未授权页面。常见做法是拆成两份路由一份是所有角色都能访问的基础路由登录页、经营总览另一份是带meta.roles标记的动态路由登录成功后根据用户角色过滤再注册。// router/routes/static.js export const staticRoutes [ { path: /login, component: () import(/views/login.vue) }, { path: /, component: () import(/layout/index.vue), redirect: /dashboard, children: [ { path: dashboard, name: Dashboard, component: () import(/views/dashboard.vue), meta: { title: 经营总览, icon: DataLine } } ] } ] // router/routes/dynamic.js export const dynamicRoutes [ { path: /room, component: () import(/layout/index.vue), name: RoomManage, meta: { title: 房态管理, icon: House, roles: [admin, reception] }, children: [ { path: calendar, name: RoomCalendar, component: () import(/views/room/calendar.vue), meta: { title: 房态日历, roles: [admin, reception] } }, { path: slot, name: RoomSlot, component: () import(/views/room/slot.vue), meta: { title: 房间维护, roles: [admin] } } ] } ]动态路由的过滤逻辑放在全局前置守卫里// router/index.js import { useUserStore } from /stores/user import { dynamicRoutes } from ./routes/dynamic router.beforeEach(async (to) { const userStore useUserStore() if (!userStore.token to.path ! /login) { return /login } if (userStore.token !userStore.routesLoaded) { const allowRoutes dynamicRoutes.filter((route) route.meta.roles.some((role) userStore.roles.includes(role)) ) allowRoutes.forEach((route) router.addRoute(route)) userStore.routesLoaded true return { ...to, replace: true } } })route.meta.roles数组里的值要和登录接口返回的角色标识保持一致。router.addRoute()会把过滤后的路由注册进路由表侧边栏组件再根据router.getRoutes()输出菜单。这样未登录用户连路由定义都拿不到URL 直闯只会落到 404 页面而不是看到一个空白或报错页。3.2 房态维度用 Pinia store 管理房态数据会被多个页面共享日历页要读、首页经营总览要读、订房弹窗也要读。把它放进 Pinia能让所有页面读同一个数据源避免各自请求接口导致数据不同步。// stores/room.js import { defineStore } from pinia import { getRoomList, updateRoomStatus } from /api/modules/room.api export const useRoomStore defineStore(room, { state: () ({ rooms: [], statusText: { 0: 仓库, 1: 空闲, 2: 占用, 3: 清洁中, 4: 维修中 }, loading: false }), actions: { async fetchRooms(params {}) { this.loading true try { const { rows } await getRoomList(params) this.rooms rows } finally { this.loading false } }, async changeStatus(roomId, status) { await updateRoomStatus({ id: roomId, status }) const target this.rooms.find((room) room.id roomId) if (target) target.status status } }, getters: { freeCount: (state) state.rooms.filter((r) r.status 1).length, dirtyRooms: (state) state.rooms.filter((r) r.status 3) } })statusText用于页面模板里做状态值到文案的映射不要把“占用”“清洁中”这类中文硬编码在业务逻辑里。changeStatus在接口成功后同步更新本地 state这样可以避免整张表重新拉取。getters 里的freeCount可以直接在房态卡片上展示“当前空闲房间数”模板里引用 store 时会自动触发依赖收集。这里有个取舍民宿房间数量通常只有几十间内存遍历开销可忽略如果房间数量上千就要考虑按楼层或分区做分页加载避免一次初始化把全量数据拉进内存。3.3 订单状态机用合法流转表约束每一步订单的流转比房态更严格不允许从“已入住”跳到“待支付”不允许“已取消”的订单再次变成“已完成”。这些约束依赖一个状态机模块来统一维护而不是在页面里散落十几处 if 判断。当前状态允许流转到触发动作pending_paypending_confirm、canceled支付回调、超时自动取消pending_confirmconfirmed、canceled前台确认订单、客人取消confirmedchecked_in、canceled办理入住、入住前取消checked_inchecked_out、canceled办理退房、取消并退款checked_outcompleted账单结算完成canceled—终态不可再流转// utils/order-status.js const ORDER_FLOW { pending_pay: [pending_confirm, canceled], pending_confirm: [confirmed, canceled], confirmed: [checked_in, canceled], checked_in: [checked_out, canceled], checked_out: [completed], canceled: [] } export function canTransit(from, to) { return (ORDER_FLOW[from] || []).includes(to) } export function nextStatus(from) { return ORDER_FLOW[from] || [] }canTransit在订单详情页渲染操作按钮时调用。比如只有当canTransit(order.status, checked_out)返回 true 时才显示“办理退房”按钮。后端接口里要再校验一遍同样的规则前端这一层只是为了减少无效请求和更早的用户反馈不能替代后端校验。4. 民宿管理系统的 Axios 封装与权限控制实现前端与后端交互这一层是民宿管理系统源码里最容易被低估的部分。如果所有页面都直接调用axios.post一旦 token 过期、接口返回业务错误码、需要统一打印日志你就得在几十个文件里逐个修改。工程化的第一步是做一个带拦截器的 request 实例。4.1 带拦截器、token 注入和错误码处理的 request 实例// api/request.js import axios from axios import { ElMessage } from element-plus import { useUserStore } from /stores/user import router from /router const service axios.create({ baseURL: import.meta.env.VITE_API_BASE || /api, timeout: 15000 }) service.interceptors.request.use( (config) { const userStore useUserStore() if (userStore.token) { config.headers.Authorization Bearer ${userStore.token} } return config }, (error) Promise.reject(error) ) service.interceptors.response.use( (response) { const { code, msg, data } response.data if (code 0) { return data } if (code 401) { const userStore useUserStore() userStore.clearToken() router.push(/login) ElMessage.warning(登录已过期请重新登录) return Promise.reject(new Error(登录已过期)) } ElMessage.error(msg || 请求失败) return Promise.reject(new Error(msg || 请求失败)) }, (error) { if (error.response error.response.status 401) { router.push(/login) } ElMessage.error( error.response ? 请求出错${error.response.status} : 网络异常 ) return Promise.reject(error) } ) export default service业务模块的接口文件只需要把 url、method、params 交给 request所有通用逻辑自动生效// api/modules/room.api.js import request from /api/request export function getRoomList(params) { return request({ url: /room/list, method: get, params }) } export function updateRoomStatus(data) { return request({ url: /room/status, method: post, data }) } export function getOrderList(params) { return request({ url: /order/list, method: get, params }) }baseURL取import.meta.env.VITE_API_BASE本地开发时在.env.development里写VITE_API_BASE/api再配合 Vite devServer 的 proxy 把请求转发到后端服务。正常响应时request 返回的已经是业务数据本身页面里用async/await或.then直接消费不需要再手动解包response.data。如果后端返回的 code 是业务错误码而非 HTTP 错误拦截器会统一弹出 Element Plus 的 error 消息页面不用在每个接口单独写错误提示。4.2 按钮级权限控制自定义指令 v-permission菜单级权限可以由路由表控制但“新增房型”按钮只有管理员能点前台看不到更好这个粒度用 v-if 写在每个组件里会非常烦人。自定义指令是更干净的方案使用场景指令写法效果页面级操作按钮v-permission[admin]非 admin 用户看不到按钮财务报表导出v-permission[admin, finance]两个角色都可见未授权访问v-permission[]元素被移除// directives/permission.js import { useUserStore } from /stores/user export const permission { mounted(el, binding) { const { value } binding if (!value || !Array.isArray(value) || value.length 0) { throw new Error(v-permission 需要接收一个非空数组例如 v-permission\[admin]\) } const userStore useUserStore() const hasPower userStore.roles.some((role) value.includes(role)) if (!hasPower) { el.parentNode el.parentNode.removeChild(el) } } }在main.js中注册import { permission } from /directives/permission app.directive(permission, permission)模板中的用法el-button v-permission[admin] typeprimary clickopenAddDialog 新增房型 /el-button el-button v-permission[admin, finance] clickexportSettlement 导出结算单 /el-button指令在mounted钩子里直接删除元素。如果权限数据是进入页面后才异步返回的会出现渲染时机问题按钮先被渲染、随后被指令移除页面会闪一下。解决办法是在登录后先把角色数据写入 Pinia再放行进入业务页面或者在指令里增加updated钩子做二次校验。4.3 房态日历的数据预处理把日期数组转成可用区间日历视图是民宿管理系统里交互最重的模块。后端接口通常返回“已被占用的日期数组”而前端日历组件需要的是“某个日期是否可订”。连续日期合并成区间可以有效减少渲染标记的次数。// utils/calendar.js import dayjs from dayjs export function buildBookedRanges(roomBookedDays) { const ranges [] let start null let end null const sorted [...roomBookedDays].sort( (a, b) dayjs(a).valueOf() - dayjs(b).valueOf() ) sorted.forEach((date, index) { const current dayjs(date) if (start null) { start current end current } else if (current.diff(end, day) 1) { end current } else { ranges.push([start.format(YYYY-MM-DD), end.format(YYYY-MM-DD)]) start current end current } if (index sorted.length - 1) { ranges.push([start.format(YYYY-MM-DD), end.format(YYYY-MM-DD)]) } }) return ranges }核心逻辑是dayjs的diff(current, day) 1判断日期是否连续。两个不相邻的日期会生成两个独立区间连续多天会合并为一个区间。日历组件拿到区间数组后把区间内的日期标记为“已订”或“不可选”同时配合当天房态做二次判断。5. 民宿管理系统的部署配置与源码调试技巧系统在本地跑通只是第一步真正让源码变得可靠的是部署和调试两端。这一章把两件事压缩成可以直接套用的配置和习惯。5.1 Nginx 部署配置与 History 路由的 rewriteserver { listen 80; server_name guesthouse.example.com; root /opt/frontend; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }try_files $uri $uri/ /index.html会把所有前端路由请求回退到index.html避免刷新页面时出现 404。/api反向代理指向后端服务让前后端同域部署规避跨域问题。注意proxy_pass结尾不要加斜杠否则路径会被重写后端拿到的是/room/list而不是/api/room/list。如果你遇到“Vue 打包后布局异常”先看根路径配置和图片资源是否用了相对路径这两个问题占了大多数。5.2 源码调试的三个实用习惯第一开发时在 Vite 配置里开启sourcemap: true浏览器调试器里看到的就是源码.vue文件而不是编译后的长行代码。正式部署前关掉避免源码直接暴露。第二把console.log封装成带环境判断的 logger 工具。民宿系统的账单逻辑涉及金额计算直接在utils/logger.js里写export function logGroup(label, data) { if (import.meta.env.DEV) { console.group(label) console.log(data) console.groupEnd() } }正式环境不会打印开发环境又能保留完整的调用现场。第三用vue-router的afterEach把每次页面跳转的路径、时间以日志形式记录。排查“客人说状态不对但说不清在哪一步操作”这类问题这条日志能直接把时间段缩小到分钟级。看到订单状态异常时先在stores/order.js里打点把 action 的入参和当前 state 一起打印确认是接口返回错误还是前端状态被错误修改再决定往哪一层查。本文还有配套的精品资源点击获取