
简介基于Vue.js与Node.js的宠物医院管理系统完整源码包适合Web全栈开发者、高校学生及项目实训覆盖预约、宠物信息、医生管理等核心业务场景。压缩包共146个文件约3.65MB以Vue单文件组件59个vue和JavaScript逻辑27个js为主同时包含图片、样式、字体及项目配置文件目录结构清晰便于按模块研究和复现。目前已有853人学习/下载具有不错的参考意义。源码内可学习Vue组件化、Vuex状态管理、Vue Router路由、Axios前后端通信等前端实践也能了解Node.js下RESTful API与JWT认证等后端思路配合ECharts、Sass等辅助技术可帮助读者系统梳理全栈项目开发与配置的关键环节。1. 从预约登记到诊疗记录宠物医院管理系统为什么需要 VueNode宠物医院的日常运营比想象中琐碎。前台要处理新客登记、老客复诊预约医生要查看宠物历史病历和疫苗记录收费台需要知道检查项目是否已开。数据散在纸本和不同电脑上就变成“明明有记录却找不到”。基于 VueNode 的宠物医院管理系统把这条链路压缩成三个动作Vue 管界面Node 提供 APIMySQL 存数据。Vue 的组件化让预约表单、宠物卡片、医生排班可以独立维护Node 的异步 I/O 能扛住并发预约请求。这套源码适合想完整走一遍全栈流程的开发者也适合做课程设计或毕业设计的人。zip 解压后前端是client目录后端是server目录下面从数据表开始拆。2. 后端基础Node Express JWT 的接口设计与数据表结构宠物医院系统后端最常见的选型是 Express 或 Koa配 MySQL 或 MongoDB。这套源码里用的是 Express MySQL理由很直接预约、客户、宠物之间是强关联关系例如一个客户可以带三只宠物就诊一只宠物可以有多条病历记录用关系型外键约束比文档型数据库更稳妥。如果业务字段不会频繁变更MySQL 的数据一致性优势明显备份和迁移也比 MongoDB 更通用。2.1 数据表设计pet、customer、doctor、appointment 的关联方式先看核心表。一个最小可运行的库至少要有四张表客户表、宠物表、医生表、预约表。关系是customer1:npetdoctor1:nappointmentpet1:nappointment。下面是建议的字段设计表名核心字段说明customerid, name, phone, address, created_at客户即宠物主人手机号做登录账号petid, customer_id, name, species, breed, birthday, medical_historycustomer_id 外键关联客户doctorid, name, title, department, schedule职称和排班用于前端展示appointmentid, pet_id, doctor_id, appointment_time, status, symptomstatus 可取值 pending / done / canceled注意phone在 customer 表里要加唯一索引因为登录时会用手机号查用户。appointment表的pet_id和doctor_id建立联合索引能加快按医生或按宠物查预约记录的速度。源码里通常在server/models或server/sql目录下能找到建表语句没有的话可以按下面的 SQL 补上CREATE TABLE pet ( id INT PRIMARY KEY AUTO_INCREMENT, customer_id INT NOT NULL, name VARCHAR(50) NOT NULL, species VARCHAR(20) DEFAULT dog, breed VARCHAR(50), birthday DATE, medical_history TEXT, FOREIGN KEY (customer_id) REFERENCES customer(id) ON DELETE CASCADE ); CREATE TABLE appointment ( id INT PRIMARY KEY AUTO_INCREMENT, pet_id INT NOT NULL, doctor_id INT NOT NULL, appointment_time DATETIME NOT NULL, status ENUM(pending, done, canceled) DEFAULT pending, symptom VARCHAR(255), FOREIGN KEY (pet_id) REFERENCES pet(id) ON DELETE CASCADE, FOREIGN KEY (doctor_id) REFERENCES doctor(id) ON DELETE CASCADE, INDEX idx_time (appointment_time) );逻辑说明pet表通过customer_id外键挂到客户下ON DELETE CASCADE表示客户注销时其名下宠物记录一起删除避免孤儿数据。appointment表通过pet_id和doctor_id建立两个外键把预约和宠物、医生关联起来。status用ENUM而不是字符串可以防止写入非法状态。idx_time索引用于查询某一天或某时段的预约列表比如前台查看“今天还有哪些预约”。参数说明species默认dog但实际项目中最好设为 NOT NULL因为猫、兔、异宠都需要区分symptom字段长度VARCHAR(255)对常见“呕吐、食欲不振”足够后续要存完整病历就改成TEXT。这些细节在源码里可能没做严格约束自己改的时候要留意。2.2 用 Express 实现 RESTful APIJWT 完成登录鉴权后端接口遵循 RESTful 风格比如GET /api/pets获取宠物列表POST /api/appointments创建预约。登录接口返回 JWT之后的请求在请求头里带Authorization: Bearer token。下面是 Express 中一个典型的 JWT 验证中间件const jwt require(jsonwebtoken); function authMiddleware(req, res, next) { const header req.headers.authorization; if (!header || !header.startsWith(Bearer )) { return res.status(401).json({ message: 未登录或 token 缺失 }); } const token header.split( )[1]; try { const decoded jwt.verify(token, process.env.JWT_SECRET); req.user decoded; next(); } catch (err) { return res.status(401).json({ message: token 过期或无效 }); } }逻辑说明先从请求头里取出Authorization要求格式是Bearer加空格加 token。jwt.verify会校验签名和过期时间校验通过后把decoded数据挂到req.user上后面的接口就能直接拿req.user.id或req.user.role做业务判断。校验失败统一返回 401前端收到后跳转登录页。参数说明process.env.JWT_SECRET在源码里通常放在.env文件不要硬编码进代码。JWT 的expiresIn一般设2h管理后台的 token 有效期比 C 端短一些降低被盗用后的风险窗口。另外jwt.verify只校验签名不检查 token 是否已注销要实现“退出登录立即失效”需要引入 token 黑名单或改用 session否则只能等它自然过期。2.3 密码存储与接口返回的一致性问题源码里常见的坑是密码明文存库。正确做法是用bcryptjs做哈希注册时bcrypt.hash(password, 10)登录时bcrypt.compare。接口返回的用户对象里要剔除password字段最好在查询语句里直接不查该列而不是查出来再手动删。如果前端拿到的用户信息里还有哈希值说明后端序列化没做干净。另一个坑是预约接口创建成功后只返回 id不返回宠物名和医生名前端列表还要再发一次请求。正确做法是返回完整的预约对象包括关联的宠物名、医生名减少前后端往返。这样后端主要模块就清楚了。接下来看 Vue 前端如何对接这些接口。3. 前端实现Vue 组件化、Vuex 状态管理与 Axios 请求封装Vue 端的作用不只是画页面而是把后端接口返回的数据变成可交互的界面。源码里前端通常是 Vue CLI 创建的标准项目目录结构包含views、components、router、store、utils。下面从路由、状态、请求封装三个层面拆。3.1 Vue Router 管理页面与路由参数页面之间的跳转离不开 Vue Router。比如从预约列表点击某条记录跳到详情页需要传递预约 id。常见的做法是用路由参数// router/index.js 路由定义 const routes [ { path: /, component: Home }, { path: /appointments, component: AppointmentList }, { path: /appointments/:id, component: AppointmentDetail, props: true }, { path: /login, component: Login }, ];跳转时列表页用this.$router.push拼上当前行 id// 列表页跳转 this.$router.push({ path: /appointments/${row.id} });详情页通过 props 接收 id并在组件创建时拉取数据// 详情页接收参数 export default { props: [id], created() { this.fetchDetail(this.id); }, };逻辑说明path: /appointments/:id表示 id 是动态段跳转时把预约 id 拼到路径里。详情页用props: true把路由参数映射为组件 prop比在组件里读this.$route.params.id更干净也方便单独测试组件。注意 id 从路由参数拿到的是字符串调用接口前要转成数字否则后端可能因类型不匹配返回 500。参数说明路由参数适合传简单 id不适合传整个对象如果要在页面间共享复杂的查询条件比如筛选状态、日期范围建议放到 query 或 Vuex 中。Vue Router 的beforeEach守卫在这里可以用来判断登录态但判断逻辑不要写在每个组件里放到路由文件统一处理。3.2 Vuex 状态管理与 Axios 请求封装登录用户的 token、用户资料、当前预约筛选条件这些跨组件要用的数据放进 Vuex。源码里 store 一般会拆成user、appointment等模块。下面是一个最小的 user 模块// store/modules/user.js import { loginApi } from /api/user; const state { token: localStorage.getItem(token) || , userInfo: null, }; const mutations { SET_TOKEN(state, token) { state.token token; localStorage.setItem(token, token); }, SET_USER_INFO(state, info) { state.userInfo info; }, }; const actions { async login({ commit }, payload) { const res await loginApi(payload); commit(SET_TOKEN, res.data.token); commit(SET_USER_INFO, res.data.user); return res; }, }; export default { state, mutations, actions };逻辑说明state.token初始化时从localStorage读取这样刷新页面后登录态不丢。loginaction 调登录接口拿到 token 后通过 mutations 写入 state 和 localStorage。注意 action 里不要直接改 state必须走 mutations这是 Vuex 的约定。Axios 封装通常在utils/request.js核心是加请求拦截器和响应拦截器import axios from axios; import router from /router; const service axios.create({ baseURL: /api, 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( (response) response.data, (error) { if (error.response error.response.status 401) { router.push(/login); } return Promise.reject(error); } ); export default service;逻辑说明请求拦截器给每个请求自动加Authorization头避免每个接口手动写 token。响应拦截器统一处理 401token 失效就跳登录页。baseURL: /api在开发时配合 devServer 代理生产环境交给 Nginx 代理避免跨域。参数说明timeout: 10000表示 10 秒超时诊所网络环境差可以调到 15000。response.data直接返回响应体后续接口拿到的就是后端返回的 JSON不用再写.data.data。localStorage存 token 有 XSS 风险项目对安全要求高可以改用内存存储加刷新接口续期但源码里用 localStorage 是常见做法。下面是 state 模块划分的参考模块存放数据典型 mutationsusertoken、用户信息、角色SET_TOKEN, SET_USER_INFO, CLEAR_USERappointment预约列表、筛选条件、当前预约详情SET_LIST, SET_FILTER, SET_DETAILpet宠物列表、当前宠物信息SET_PETS, SET_CURRENT_PET3.3 组件化开发以预约挂号表单为例预约挂号页面是前后端交互最密集的地方。前端需要一个表单包含宠物选择、医生选择、时间、症状描述。宠物和医生的下拉数据分别来自GET /api/pets和GET /api/doctors。组件设计上AppointmentForm.vue负责收集表单PetSelect.vue和DoctorSelect.vue负责各自的数据加载。关键点在提交时同时校验宠物、医生、时间三个字段后端也要再校验一次避免提交空数据。源码里如果用了 Element UI 或 Ant Design Vueel-form的校验规则可以配trigger: blur减少输入过程中的频繁提示。子组件只负责展示和选择把选中的 id 通过$emit传给父组件父组件最后统一调POST /api/appointments。这样后续要增加“预约多只宠物”或“选择分院”时只需要扩展选中的值结构不用重写表单。4. 权限与数据可视化ECharts 统计图与 JWT 的角色控制管理系统不能只有增删改查。宠物医院的老板要看营业数据医生要看自己的接诊量所以权限控制和数据可视化是这里的亮点。权限控制不是简单的“登录就能看”而是要区分管理员、医生、前台等角色。4.1 前端角色权限控制路由守卫与菜单过滤JWT 里通常包含角色信息比如{ id: 3, role: doctor }。前端在跳转路由时根据角色判断是否允许访问。下面是一个简单的路由守卫示例router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.path /login) { next(); return; } if (!token) { next(/login); return; } const role localStorage.getItem(role); if (to.meta.roles !to.meta.roles.includes(role)) { next(/403); return; } next(); });逻辑说明to.meta.roles在路由定义时声明比如/admin路由声明roles: [admin]/appointments声明roles: [admin, doctor, receptionist]。守卫先检查 token再检查角色数组是否包含当前用户角色。角色信息存在 localStorage 里用户可能手动修改所以这只是一种体验优化真正的权限校验必须以后端接口返回为准。角色可访问页面典型接口admin全部分页、统计、系统设置全部 GET/POST/DELETEdoctor预约列表、宠物详情、个人统计GET /api/appointmentsGET /api/stats/appointments?scopemereceptionist预约创建、客户查询、宠物登记POST /api/appointmentsGET /api/customers参数说明这里的角色字段名要和后端 JWT 里一致比如后端签发时用role前端也必须用role。如果改了字段名前后端不同步会出现“有 token 但一直跳 403”的情况。菜单过滤可以维护一份独立配置每个菜单项声明allowedRoles渲染时用v-if判断避免把角色判断散落在每个页面。4.2 对接统计接口ECharts 动态渲染预约趋势数据统计方面源码里通常用 ECharts 画预约量趋势折线图和宠物种类占比饼图。后端需要提供聚合接口例如GET /api/stats/appointments?days7返回近 7 天每天的预约数量。前端拿到数据后渲染// views/Dashboard.vue import * as echarts from echarts; import { getAppointmentStats } from /api/stats; async function loadStats() { const res await getAppointmentStats({ days: 7 }); const dates res.map(item item.date); const counts res.map(item item.count); const chart echarts.init(document.getElementById(chart)); chart.setOption({ xAxis: { type: category, data: dates }, yAxis: { type: value }, series: [{ type: line, data: counts }], }); }逻辑说明getAppointmentStats返回的数组里每一项有date和count字段分别映射到横轴和纵轴。echarts.init要等 DOM 渲染完成后再调用所以一般在mounted里执行。chart.setOption是 ECharts 的核心方法第一次传完整配置后续更新数据可以只传变化部分。参数说明xAxis数据需先排序后端聚合要按日期GROUP BY排序否则折线会乱序。count字段在 SQL 里用COUNT(*)但某一天可能零记录导致折线断开。常见做法是后端把缺失日期补 0或在 SQL 里生成连续日期序列。源码里如果没有补 0前端也可以自己遍历日期区间。提示ECharts 实例在组件销毁时要调用chart.dispose()避免切换页面后内存泄漏。Vue 组件的beforeDestroy钩子里清理即可。4.3 将权限与图表结合按医生维度过滤数据统计图不一定要全局展示。医生登录后系统应该只显示自己的接诊数据。接口可以设计为GET /api/stats/appointments?doctor_id当前用户id但更安全的做法是不传doctor_id后端从 token 里解析用户角色和 id如果是医生角色就强制只查自己的数据。这样即使用户伪造请求也拿不到别人的数据。这段逻辑应该放在 Node 路由层的中间件里而不是散落在业务代码中。前端只需要根据角色显示不同的标题和筛选入口管理员能看到全部医生只能看到“我的接诊量”。5. 部署验证从 zip 源码到 Nginx 反向代理的完整检查单拿到 zip 之后最怕的不是代码跑不起来而是不知道启动顺序和端口配置。常规做法是前端开发服务器用 8080后端用 3000。源码里如果已经配了代理可以按下面的步骤走。5.1 解压后的目录与本地启动顺序先确认目录通常包含client和server两个文件夹。依次执行cd server cp .env.example .env # 配置数据库连接和 JWT_SECRET npm install npm run dev # 默认监听 3000 端口cd client npm install npm run serve # 默认监听 8080 端口逻辑说明后端先启动前端开发服务器再启动这样前端代理才能把/api转发到可用的后端进程。cp .env.example .env是复制环境变量样例必须修改数据库名、密码和 JWT_SECRET。参数说明如果client的vue.config.js里配置了devServer.proxy前端请求/api会自动转发到http://localhost:3000本地不用处理跨域。.env里的数据库密码和 JWT_SECRET 不要提交到 Git源码 zip 里如果包含.env部署前必须改掉默认值。5.2 Nginx 反向代理与跨域验证生产环境前端构建后需要把/api路径代理到 Node 服务。Nginx 配置可以这样写server { listen 80; server_name pet.example.com; location / { root /var/www/pet-web/dist; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }逻辑说明location /api/把带/api前缀的请求转发到 Node 服务proxy_set_header是为了让后端获取真实客户端 IP。try_files是前端 history 路由的关键找不到文件时回退到 index.html否则刷新子页面会 404。参数说明proxy_pass http://127.0.0.1:3000;末尾加不加斜杠会导致代理路径不同如果后端接口路由统一带/api前缀保持原样即可。server_name要结合实际域名或服务器 IP。验证方法后端启动后在本地直接curl -X POST http://localhost:3000/api/auth/login -H Content-Type: application/json -d {phone:13800000000,password:123456}返回 JWT 说明后端接口正常再访问http://localhost:8080/api/pets能返回 JSON 说明开发代理正常部署后访问http://pet.example.com/api/pets能通说明反代配置正确。注意如果遇到端口被占用直接改.env里的PORT但要同步修改 Nginx 的proxy_pass地址不要只改一处。调试时优先看后端日志和请求头里的Authorization定位会快很多。本文还有配套的精品资源点击获取