恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
基于Vue3与Pinia的自习室预约系统前端设计与实现
首页
资讯中心
/
基于Vue3与Pinia的自习室预约系统前端设计与实现
基于Vue3与Pinia的自习室预约系统前端设计与实现
发布时间:2026/9/15 5:15:05
简介这是基于Vue框架开发的一套自习室预约系统前端源码适用于需要掌握Vue组件化开发与前端工程化实践的初中级开发者也可作为高校课程设计或毕业设计的参考项目。资源包共49个文件整体约593KB包含35个Vue组件文件、5个JavaScript脚本、2个JSON配置以及HTML、PNG图片、ICO图标、Git忽略文件等覆盖了预约表单、座位选择、时间调度、后台管理等典型业务模块src与public目录划分清晰便于定位代码和静态资源。Vue组件负责界面渲染与交互JavaScript脚本封装请求与处理逻辑JSON文件定义接口地址与项目配置store与router等目录展示了状态管理和路由跳转的常见用法便于读者从零理解一个中后台前端应用的构成。目前已有86人学习适合通过实战代码学习Vue框架的核心特性与前端开发最佳实践。1. 基于Vue框架的自习室预约系统前端设计源码看起来像课程设计解决的却是很实际的痛点座位紧张时学生到现场才发现没座管理员的登记表永远慢半拍。这套前端设计源码把选座流程搬进浏览器——用Vue渲染可点击的座位分布图用路由切分列表、选座、我的预约三个页面用状态管理同步座位是空是满这份实时数据。要覆盖的是信息透明度和并发选座冲突。适合三类人Vue课设初学者、图书馆或共享办公预约模块的前端、从管理后台转做C端预约界面的工程师。下文围绕Vue 3 Vue Router Pinia这组当前最稳的前端组合展开会npm命令就能落地。2. Vue项目初始化与自习室预约系统路由设计2.1 用create-vue初始化项目并装好依赖跳过脚手架版本纠结直接用官方create-vue初始化。先交代运行环境Node.js 18以上npm 9以上这两项直接去官网装LTS版本就行。新项目创建命令如下npm create vuelatest study-room-front执行后会有几个交互式选项问我是否启用Router、Pinia、ESLint等等把Router和Pinia选上TS看团队习惯不熟悉类型就先不用。这一步会生成一个带router和stores目录的最小工程比自己手动装vue-router再配置省事得多。继续装项目运行时依赖cd study-room-front npm install npm install axios element-plus这里的element-plus是Vue 3生态里使用面最广的前端组件库预约系统里用得最多的是按钮、表单、弹窗、消息提示这几类不至于为几个小组件引入一堆自研代码。axios用来统一处理预约接口的请求和响应后面提交预约、拉取座位列表都走它。如果团队更倾向用fetch可以跳过axios但拦截器、错误码统一处理这些要自己写。注意拿到一份现成的源码时先看package-lock.json再执行npm install不要把锁文件删了重装锁文件里确定的间接依赖版本能最大程度复现作者环境避免装出另一个版本排查半天。代码逻辑说明create-vue本身不做依赖安装所以创建完毕后必须先npm install。这里列出的三条命令执行完基础工程就能跑起来启动开发服务器用npm run dev。参数说明npm create vuelatest后面的study-room-front是项目目录名可自定义Router和Pinia的选项决定有没有router/index.js和stores目录选上之后后续的路由和状态管理章节直接在这个基础上写。2.2 自习室预约系统的路由表该怎么拆预约系统的页面不多但路径关系如果没理清后面加管理员查预约记录查看某个自习室详情都会变得混乱。我一般按四个层次拆路由公共页登录、自习室浏览列表详情、预约操作选座确认、个人中心我的预约。对应关系如下路由路径页面组件页面标题需要登录/homeHomeView.vue自习室列表否/room/:idRoomDetailView.vue座位选择否/reservationReservationView.vue我的预约是/loginLoginView.vue登录否路由配置文件// src/router/index.js import { createRouter, createWebHistory } from vue-router const routes [ { path: /, redirect: /home }, { path: /home, name: Home, component: () import(/views/HomeView.vue), meta: { title: 自习室列表, requireAuth: false } }, { path: /room/:id, name: RoomDetail, component: () import(/views/RoomDetailView.vue), props: true, meta: { title: 座位选择, requireAuth: false } }, { path: /reservation, name: Reservation, component: () import(/views/ReservationView.vue), meta: { title: 我的预约, requireAuth: true } }, { path: /login, name: Login, component: () import(/views/LoginView.vue), meta: { title: 登录, requireAuth: false } } ] const router createRouter({ history: createWebHistory(), routes }) router.beforeEach((to) { document.title ${to.meta.title} - 自习室预约系统 if (to.meta.requireAuth !localStorage.getItem(token)) { return { path: /login, query: { redirect: to.fullPath } } } }) export default router逻辑说明这里把方法名写全createWebHistory、createRouter就是为了后面打包部署时一眼能看出用了哪种history模式。props: true是Vue Router 4里很省事的写法组件里直接用defineProps([id])拿到自习室ID不用再写const route useRoute()手动取值。meta字段承担两个职责页面标题文案、是否需要登录校验收比写到组件里再逐页判断清晰。参数说明query.redirect记录的是用户想去的原始路径登录成功后可以用router.push(route.query.redirect)跳回去这个细节很多源码里没做导致用户登录后永远回到首页还要再点一次自习室链接。另一个参数是createWebHistory()地址栏没有#刷新行为依赖后端配合第5章会展开讲nginx配置。如果你拿到的源码里路由参数经常在组件里取不出来多半是用了/room/:id但没有给组件加props: true或者组件里同时用了defineProps和useRoute两种方式把自己绕进去了。建议一个页面统一用一种取值方式。要不要把路由按模块拆成多个文件项目超过20个页面可以拆预约系统这种10个页面以内的规模单文件就足够拆了反而要点开两三个文件才能看全路径关系。3. 自习室座位状态管理与预约组件拆分3.1 为什么用Pinia管座位状态而不是组件props选座场景里状态是座位当前是空闲、已被预约还是已被占用。这个状态在座位图组件里要渲染颜色在弹窗里要展示文字提交完还要回到座位图刷新三个组件共享同一份数据。用props逐级传A组件改了状态B组件要等父组件重渲染才能拿到新的中间一旦插入弹窗或路由跳转响应就断了。这也是为什么Pinia这类全局状态管理在预约系统里不是加分项而是必需品。Pinia的写法比Vuex简单不需要mutations那一层直接改值就行。预约系统的全局状态我一般拆成两个storeseat管座位状态user管登录信息。座位store的结构// src/stores/seat.js import { defineStore } from pinia export const useSeatStore defineStore(seat, () { // 座位状态集合key为 roomId-seatIdvalue为状态码 const seatMap ref({}) const selectedSeat ref(null) function initSeats(roomId, seats) { // 进入自习室页面时把后端返回的座位列表写入store seats.forEach((seat) { seatMap.value[${roomId}-${seat.id}] seat.status }) } function setSeatStatus(roomId, seatId, status) { seatMap.value[${roomId}-${seatId}] status } function selectSeat(seat) { selectedSeat.value seat } function clearSelection() { selectedSeat.value null } return { seatMap, selectedSeat, initSeats, setSeatStatus, selectSeat, clearSelection } })逻辑说明这里用的是setup store写法跟组件里写ref的习惯一致比options store少一层state/actions包裹。seatMap的key合并了roomId和seatId是为了支持同时打开两个自习室标签页时不互相覆盖。initSeats是整批初始化适合进入自习室详情页时拉全量座位setSeatStatus是单点更新适合预约成功后把对应座位改成reserved不用重新请求座位图。参数说明status三个约定值free、occupied、reserved要和后端接口文档统一前端只认这三个字符串后端返回其他值都按空闲处理但要打日志方便排查。3.2 座位卡片组件与状态样式映射座位图页面通常是一张网格每个格子是一个座位。把座位卡片的渲染交给自己封装的组件状态样式切换、键盘支持、点击行为都收敛在一个文件里这也是前端组件库封装时最常见的拆法一个原子组件只做一件事。座位状态映射关系先定好status值业务语义按钮是否可用样式类名free空闲可选可点击seat-freeoccupied已签到占用禁用seat-occupiedreserved已被预约禁用seat-reserved组件模板和脚本!-- src/components/SeatCard.vue -- template button classseat-card :classstateClass :disabledseat.status occupied || seat.status reserved clickhandleClick {{ seat.label }} /button /template script setup import { computed } from vue const props defineProps({ seat: { type: Object, required: true } }) const emit defineEmits([select]) const stateClass computed(() { const map { free: seat-free, occupied: seat-occupied, reserved: seat-reserved } return map[props.seat.status] || seat-free }) function handleClick() { if (props.seat.status ! free) return emit(select, props.seat) } /script参数说明seat对象需要包含id座位主键、label展示给用户看的座位号、status当前状态三个必填字段。stateClass用computed把status字符串映射成CSS类名把样式判断集中一处后面要加维护中状态只改这一处。按钮的disabled同时覆盖了占用和预约两种状态也避免了handleClick里的重复判断。注意一个常见的Vue样式坑如果座位卡片的背景色写在style scoped里并且通过父组件传入class去切换scoped属性会让类选择器的作用范围变窄切换经常不生效。我一般把seat-free、seat-occupied这些状态类放到全局样式里或者用:deep()处理这样子组件根节点才能稳定命中。颜色上free用绿色occupied用红色reserved用黄色。对比度要做到色弱用户也能区分除了颜色本身再配合按钮内的文字做第二重提示比如reserved座位上显示一个约字。父组件里循环渲染座位卡片点击时写入store// src/views/RoomDetailView.vue function onSeatClick(seat) { seatStore.selectSeat(seat) // 跳转预约确认页或者打开底部弹窗按产品设计来 router.push(/reservation/confirm) }这里的要点是选座这个动作只更新selectedSeat这个全局选中值不改seatMap里的状态。真正把free改成reserved发生在提交接口成功之后通过setSeatStatus同步。如果选座时就改了状态用户中途退出页面座位会一直显示为已选实际数据库里还是空闲前后端状态就对不上了。4. 自习室预约流程与时间片冲突处理4.1 时间片组件与v-model双向绑定选完座位之后是选时间。预约系统的时间粒度不一定是分钟级常见的是把一天切成几个固定时段比如早中晚各一段后端只认这几个时段前端就不需要在时间计算上花太多心思。这也是前端开发里传参和展示分离的常见做法时间段怎么展示由前端拼时间段怎么存储由后端定。时间片组件接受一个slots数组用v-model同步选中值。实现要点是把选中状态和点击取消都收敛在组件内部template div classtime-slots button v-forslot in slots :keyslot.id typebutton :class{ slot-active: modelValue slot.id } clickhandleSelect(slot) {{ slot.startTime }}-{{ slot.endTime }} /button /div /template script setup const props defineProps({ modelValue: { type: String, default: }, slots: { type: Array, required: true } }) const emit defineEmits([update:modelValue]) function handleSelect(slot) { if (props.modelValue slot.id) { // 再次点击取消选中 emit(update:modelValue, ) return } emit(update:modelValue, slot.id) } /script逻辑说明v-model在组件层面的本质是modelValue加update:modelValue事件所以组件内部选中、取消都通过emit通知父组件组件自身不维护一个副本避免和父级不同步。handleSelect里增加了重复点击取消的逻辑预约场景下误选时段后能直接取消再选不用刷新页面。参数说明slots每项至少含id、startTime、endTime时间格式统一用24小时制HH:mm前端只做展示和传参不做时间加减避免引入时区与夏令时的边界问题。如果自习室预约系统要支持跨天预约多个时段可以引入Element Plus的el-date-picker用typedaterange选日期段再配合上面的时段组件逐日生成预约请求。这里的坑在于跨天和闭馆时刻的组合闭馆时间到次日开馆之间不能生成预约条目。实际产品里一般做成选日期段选固定时段比如连续约3天、每天10:00-12:00每天生成一个独立预约条目提交。4.2 提交预约的请求组装与冲突兜底时间片确定后前端传参的核心是请求体结构。提交预约的请求要包含五类信息自习室ID、座位ID、预约日期、时间片ID、备注。提交前的前置校验按用户操作顺序来做先座位、再日期、再时段提示信息一句到位不要用大段的表单校验文案。// src/composables/useReservation.js import { reactive, ref } from vue import { ElMessage } from element-plus export function useReservation(roomId, seatStore) { const loading ref(false) const selectedDate ref() const selectedTimeId ref() const form reactive({ remark: }) async function submit() { if (!seatStore.selectedSeat) { ElMessage.warning(请先选择座位) return } if (!selectedDate.value) { ElMessage.warning(请选择预约日期) return } if (!selectedTimeId.value) { ElMessage.warning(请选择预约时段) return } loading.value true try { const res await fetch(/api/reservation/create, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ roomId, seatId: seatStore.selectedSeat.id, date: selectedDate.value, timeId: selectedTimeId.value, remark: form.remark.trim() }) }).then((r) r.json()) if (res.code 0) { seatStore.setSeatStatus(roomId, seatStore.selectedSeat.id, reserved) seatStore.clearSelection() ElMessage.success(预约成功) } else { ElMessage.error(res.message) } } catch (e) { ElMessage.error(网络异常请稍后重试) } finally { loading.value false } } return { loading, selectedDate, selectedTimeId, form, submit } }参数说明请求体里单独传roomId是为了后端校验这个座位是否确实属于这个自习室只靠座位ID也能查到但多一次表关联不如前端直接传。date字段必须是YYYY-MM-DD格式的字符串timeId是后端时间片表的主键后端拿到后反查起止时间再做区间重叠判断。res.code 0这里是个约定示例实际项目按后端返回结构调整重点是区分业务失败和网络失败两种错误业务失败时不要清空用户已填的表单。冲突兜底两个学生同时提交同一个座位的同一时段后端一定会有一个失败。前端收到失败提示后不能只弹个错误还应该重新拉一次座位图把对方已经抢到的座位刷成occupied或reserved状态。我一般这样写async function refreshSeats(roomId) { const res await fetch(/api/room/${roomId}/seats).then((r) r.json()) if (res.code 0) { seatStore.initSeats(roomId, res.data) } }这个刷新动作放在冲突失败分支里跟预约成功本地更新形成对照组成功时本地改一个座位状态就够了失败时整批覆盖数据。多的这一次请求换来的是一次干净的交互体验。提示预约系统的前后端时间格式约定要写进接口文档前端用HH:mm字符串提交后端统一转成分钟数存储避免格式解析差异引发的冲突误判。5. 打包部署后布局异常与路由404的排查技巧5.1 路由模式与nginx配置开发环境一切正常、npm run build打包后部署到服务器刷新/room/1出现404这是历史路由模式加上nginx没有配置回退的表现。nginx层的修法location / { try_files $uri $uri/ /index.html; }try_files按顺序尝试先按请求路径找文件再找目录都没有就回退到/index.html由前端路由接管渲染对应页面。配置完记得nginx -t检查语法再reload。如果项目部署在服务器的子目录比如https://example.com/study/只有nginx这条还不够vite.config.js里要把base改掉import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], base: /study/ // 部署到 /study/ 子路径时使用 })5.2 Vue打包后布局异常的常见原因Vue打包后布局异常的案例里自习室预约系统最常见的是这三类。第一类是Element Plus样式丢失通常控制台能看到一堆404资源请求先检查dist目录下assets路径和页面是否同级。第二类是座位卡片挤在一起不换行打开Elements面板看父容器的display和flex-wrap开发环境浏览器窗口宽度和手机上不一致卡片要用固定宽度而不是百分比。第三类是图标变成小方块检查dist里fonts目录是否存在以及CSS里字体路径是不是被解析成了绝对路径。遇到这类问题我习惯用Chrome DevTools的Network面板加Disable cache刷新页面看资源加载顺序。HTML和JS都加载成功但页面空白多半是运行时JS报错切Console看红色报错HTML直接404问题在base路径或nginx location配置。如果只想快速判断是不是history模式问题把路由临时改成createWebHashHistory再打包一次能正常刷新访问就说明是后端回退配置少了。5.3 用npm run preview做部署前验证打包产物部署前有一个本地验证手段npm run preview。它会启动一个静态服务器服务dist目录行为最接近生产环境。preview能正常点击、刷新路由问题基本就集中在nginx配置或域名子路径preview里就404或样式错乱直接回到vite.config.js查base回到组件里查CSS作用域。这个验证能省掉反复上传服务器的等待值得每次打包后固定执行。本文还有配套的精品资源点击获取