Vue+Node宠物医院管理系统源码解析
2026/9/17 3:21:33 网站建设 项目流程

简介:基于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. 从预约登记到诊疗记录:宠物医院管理系统为什么需要 Vue+Node

宠物医院的日常运营比想象中琐碎。前台要处理新客登记、老客复诊预约,医生要查看宠物历史病历和疫苗记录,收费台需要知道检查项目是否已开。数据散在纸本和不同电脑上,就变成“明明有记录却找不到”。基于 Vue+Node 的宠物医院管理系统,把这条链路压缩成三个动作:Vue 管界面,Node 提供 API,MySQL 存数据。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_iddoctor_id建立联合索引,能加快按医生或按宠物查预约记录的速度。源码里通常在server/modelsserver/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_iddoctor_id建立两个外键,把预约和宠物、医生关联起来。statusENUM而不是字符串,可以防止写入非法状态。idx_time索引用于查询某一天或某时段的预约列表,比如前台查看“今天还有哪些预约”。

参数说明:species默认'dog',但实际项目中最好设为 NOT NULL,因为猫、兔、异宠都需要区分;symptom字段长度VARCHAR(255)对常见“呕吐、食欲不振”足够,后续要存完整病历就改成TEXT。这些细节在源码里可能没做严格约束,自己改的时候要留意。

2.2 用 Express 实现 RESTful API,JWT 完成登录鉴权

后端接口遵循 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.idreq.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 创建的标准项目,目录结构包含viewscomponentsrouterstoreutils。下面从路由、状态、请求封装三个层面拆。

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 一般会拆成userappointment等模块。下面是一个最小的 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。响应拦截器统一处理 401,token 失效就跳登录页。baseURL: '/api'在开发时配合 devServer 代理,生产环境交给 Nginx 代理,避免跨域。

参数说明:timeout: 10000表示 10 秒超时,诊所网络环境差可以调到 15000。response.data直接返回响应体,后续接口拿到的就是后端返回的 JSON,不用再写.data.datalocalStorage存 token 有 XSS 风险,项目对安全要求高可以改用内存存储加刷新接口续期,但源码里用 localStorage 是常见做法。

下面是 state 模块划分的参考:

模块存放数据典型 mutations
usertoken、用户信息、角色SET_TOKEN, SET_USER_INFO, CLEAR_USER
appointment预约列表、筛选条件、当前预约详情SET_LIST, SET_FILTER, SET_DETAIL
pet宠物列表、当前宠物信息SET_PETS, SET_CURRENT_PET

3.3 组件化开发:以预约挂号表单为例

预约挂号页面是前后端交互最密集的地方。前端需要一个表单,包含宠物选择、医生选择、时间、症状描述。宠物和医生的下拉数据分别来自GET /api/petsGET /api/doctors。组件设计上,AppointmentForm.vue负责收集表单,PetSelect.vueDoctorSelect.vue负责各自的数据加载。关键点在提交时同时校验宠物、医生、时间三个字段,后端也要再校验一次,避免提交空数据。源码里如果用了 Element UI 或 Ant Design Vue,el-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/DELETE
doctor预约列表、宠物详情、个人统计GET /api/appointments,GET /api/stats/appointments?scope=me
receptionist预约创建、客户查询、宠物登记POST /api/appointments,GET /api/customers

参数说明:这里的角色字段名要和后端 JWT 里一致,比如后端签发时用role,前端也必须用role。如果改了字段名,前后端不同步,会出现“有 token 但一直跳 403”的情况。菜单过滤可以维护一份独立配置,每个菜单项声明allowedRoles,渲染时用v-if判断,避免把角色判断散落在每个页面。

4.2 对接统计接口:ECharts 动态渲染预约趋势

数据统计方面,源码里通常用 ECharts 画预约量趋势折线图和宠物种类占比饼图。后端需要提供聚合接口,例如GET /api/stats/appointments?days=7,返回近 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返回的数组里每一项有datecount字段,分别映射到横轴和纵轴。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 解压后的目录与本地启动顺序

先确认目录,通常包含clientserver两个文件夹。依次执行:

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。

参数说明:如果clientvue.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,定位会快很多。

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

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

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

立即咨询