简介:基于 Vue 的医疗器械检测项目设计源码,面向 Vue 开发者及医疗器械信息化项目设计人员,提供可复用的组件化前端工程参考。资源共 118 个文件,压缩包约 440KB,包含 44 个 Vue 组件、39 个 JavaScript 脚本、11 个 SVG 图形、6 个 SCSS 样式表,以及 YML 配置、JSON 数据、PNG 图片、Markdown 文档等类型。Vue 组件划分页面模块,JavaScript 实现交互逻辑,SCSS 统一视觉风格,YML 与 JSON 支撑环境配置和数据交换,目录结构清晰,从用户界面到配置管理均遵循现代 Web 开发实践,适合系统学习组件化开发、状态处理与多环境配置。项目已附带说明文档,可快速了解安装、配置与运行方式。目前已有 90 人学习下载,对医疗器械检测系统开发具有实践参考价值。
1. 基于 Vue 的医疗器械检测项目,源码先从设备台账写起
检测机构的系统列表里,设备台账往往比订单管理更容易让页面失控。一台灭菌器要记录制造商、型号、出厂编号、计量校准日期、下次检定日期,还要把检测任务关联到这台设备上,报告才能写清楚“用哪台设备、按哪个标准做的检测”。这类项目如果继续用多页 jQuery 维护,页面之间传参靠 localStorage,三个月后新同事很难接手。基于 Vue 的医疗器械检测项目设计源码,常见的起点就是先用组件把事情拆开:设备、任务、报告各占一个模块,再通过 Vue Router 的路由参数和接口层把它们串成一条可追溯的单据流。这里要讲的是 Vue 在这个领域的工程分层、核心页面实现、检测视频回放以及打包阶段的坑,适合要交付或接手这类源码的前后端工程师。
2. 医疗器械检测 Vue 源码的工程与数据层,先把路由和接口约定死
设备、任务、报告三个实体一旦拆开,第一个要决定的就是工程用什么架子。建议按团队最熟的组合走,常见做法是 Vue 3 + Vite + Element Plus + Pinia,检测项目页面以表格、表单、详情页为主,不需要太重的前端基础设施。选择 Vue 3 的理由主要是组合式 API 能让“设备到期提醒”这种逻辑集中在一个函数里,比 Vue 2 的 options API 更便于复制到下一个项目;后端语言不强求,Spring Boot、Go、Node 都能对接,前端只认接口返回的 JSON。下面从选型、路由、状态三件事把数据层定死,后面加页面才不会各写各的。
2.1 先评估 Vue 2 还是 Vue 3:检测项目更看重组合复用
有的检测机构现有系统还在用 Vue 2 + Element UI,如果只是加一个设备台账模块,继续用 Vue 2 是合理的。如果是重新设计源码,建议直接上 Vue 3。选型时比较常讨论的点列成表格看一下:
| 对比项 | Vue 3 + Vite | Vue 2 + Webpack |
|---|---|---|
| 组合式 API | 支持,检测逻辑可以直接抽成 hooks | 需要 @vue/composition-api,维护成本偏高 |
| TypeScript | 一等公民,报告模板和检测项目字段有类型提示 | 需要额外配置,老项目接入成本高 |
| 构建速度 | 冷启动快,中大型检测报告页面也能接受 | 依赖越多越慢,HMR 在大的 report 表单页会卡 |
| 生态 | Element Plus、ant-design-vue 都是新版本 | Element UI 已进入维护模式,问题只修不新增特性 |
这个表格想要说明的结论是:不是 Vue 3 一定好。如果团队长期维护 Vue 2 的业务基座,而且这次只是给医疗器械检测项目补一个报告预览页,保持同一个框架比换新框架更能降低回归风险。反过来,只要是从零搭设计源码,选 Vue 3 能让“设备到期状态、任务流转、报告模板”这类纯前端逻辑更容易被后面的人接手,也更好在面试里讲清楚为什么这么定。
2.2 安装依赖与 Vue Router 路由参数设计:用 URL 把单据串起来
工程创建之后,先装路由、状态管理和 UI 库。安装前先确认 Node 版本能跑到 Vite 的要求,通常需要 Node 18 以上,否则依赖下载完会出现奇怪的 EINVALIDTAGNAME 错误。命令如下:
npm create vue@latest med-device-web cd med-device-web npm install npm install axios pinia element-plus vue-router@4这段命令先通过 create-vue 脚手架生成基础工程,再安装运行和构建需要的最小依赖。vue-router 的版本要和 Vue 3 匹配,这里写 vue-router@4;element-plus 直接引正式包,不要用 alpha 版本,否则表格组件的列宽设置在不同小版本之间容易变,换行会出现参差不齐的布局。
路由参数怎么设计,直接影响报告和任务之间的跳转是否好追溯。常见做法是把业务主键放在 URL 参数里,页面刷新后还能根据参数恢复现场:
const routes = [ { path: '/', component: Layout, children: [ { path: 'devices', name: 'DeviceList', component: () => import('@/views/device/DeviceList.vue') }, { path: 'devices/:id', name: 'DeviceDetail', component: () => import('@/views/device/DeviceDetail.vue'), props: true }, { path: 'tasks/:taskId', name: 'TaskDetail', component: () => import('@/views/task/TaskDetail.vue'), props: true }, { path: 'report/:taskId', name: 'Report', component: () => import('@/views/report/ReportPage.vue'), props: true }, ], }, ]这段配置说明了两个点:第一,任务详情页从列表跳过来,页面组件里用route.params.taskId或者props.taskId拿到任务号,再去请求检测项目、样品信息、设备信息;第二,报告地址直接挂任务号,方便后续把 PDF 链接复制给质量负责人,对方不用从头找。如果某个检测任务下面包含多个样品,建议再加:sampleId,但不要把一个任务的全部数据都放到 query 里,URL 会很长,而且审计时容易把敏感字段暴露在浏览器历史记录中。
路由配置完后,页面里的跳转一般写成:
router.push({ name: 'TaskDetail', params: { taskId: row.id } })这里用 name 而不是 path,是为了后续接口改造如果不改名字,只改路由的 path,页面跳转代码就不必跟着改。检测项目经常要调整菜单层级,这种写法能少改一批router.push。
2.3 接口层与 Pinia 状态:让检测数据在组件外先成型
医疗器械检测项目的核心页面基本是“列表查出来,点进去看详情,提交以后变状态”。直接在每个页面里写 axios 请求会导致同一个设备接口被三四个人复制,后端字段一变就要全文替换。这里给一个接口工厂的常见写法:
import axios from 'axios' const service = axios.create({ baseURL: import.meta.env.VITE_API_BASE, timeout: 15000, }) service.interceptors.request.use((config) => { const token = localStorage.getItem('med_token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) service.interceptors.response.use( (response) => response.data, (error) => { if (error.response?.status === 401) { window.location.href = '/login' } return Promise.reject(error) } ) export default service这里的baseURL从环境变量读,是因为检测机构的内网部署环境和云端环境往往不是一个域名,测试环境的报告接口可能直接挂在办公室内网 IP 上。拦截器在这里统一处理 token,报告导出和视频回放也需要带上同一个身份信息,后面做鉴权时就不用每个业务模块单独判断。
需要维护设备、任务、报告多份状态时,用 Pinia 比较轻。下面用一个检测任务的 store 表示状态流转:
import { defineStore } from 'pinia' export const useTaskStore = defineStore('task', { state: () => ({ currentTask: null, status: 'PENDING', }), actions: { async loadTask(taskId) { const data = await api.get(`/tasks/${taskId}`) this.currentTask = data this.status = data.status return data }, async submit(taskId, payload) { await api.post(`/tasks/${taskId}/submit`, payload) this.status = 'SUBMITTED' }, }, })store 的意义在于检测任务的“提交中”“已退回”“已审核”等状态会被设备页、报告页、任务列表页同时读取,如果各自维护一份本地变量,一个动作更新后其他页面还要刷新才能看到最新结果。通过 Pinia 把当前任务放到 store 里,报告页的computed能直接根据status控制按钮是否可点击。
把路由参数、接口层、状态管理这三件事定死,等于给项目划好了边界,后面加“导出记录”“批量送检”都只是往这层里塞方法,不会让页面互相拉着状态跑。
3. 医疗器械检测核心页面源码:设备台账、任务流转、报告生成
在工程骨架之上,接下来写三个最能体现检测业务特点的页面:设备台账、检测任务、检测报告。这三个页面不是简单 CRUD,关键在于状态计算、流转控制、打印样式三件事。下面每个小节都给出可以直接放进src/views目录的代码片段,并说明改哪些字段能适配不同的检测项目。
3.1 设备台账页面:把校准到期做成 Vue 计算样式
设备台账的核心字段通常包括设备编号、名称、型号、计量检定有效期,以及最近一次检定日期。一个常见需求是“距离校准过期不足 30 天的设备要在列表里标红”。这个逻辑放在前端computed里最直观,因为后端返回的nextCalDate字段一旦有值,前端可以根据当天日期实时刷新,不必每天跑定时任务改数据库。
<template> <el-table :data="dueSoonDevices" row-key="id" border> <el-table-column prop="deviceNo" label="设备编号" width="120" /> <el-table-column prop="name" label="设备名称" min-width="180" /> <el-table-column prop="nextCalDate" label="下次检定日期" width="140"> <template #default="{ row }"> <span :class="{ 'cal-warning': isCalDue(row.nextCalDate) }"> {{ row.nextCalDate }} </span> </template> </el-table-column> <el-table-column label="操作" width="160"> <template #default="{ row }"> <el-button size="small" @click="goDetail(row.id)">查看</el-button> </template> </el-table-column> </el-table> </template> <script setup> import { computed } from 'vue' import { useRouter } from 'vue-router' import { useDeviceStore } from '@/stores/device' const router = useRouter() const deviceStore = useDeviceStore() const dueSoonDevices = computed(() => deviceStore.devices.filter((d) => d.nextCalDate) ) function isCalDue(dateStr) { const days = (new Date(dateStr).getTime() - Date.now()) / 86400000 return days >= 0 && days <= 30 } function goDetail(id) { router.push({ name: 'DeviceDetail', params: { id } }) } </script> <style scoped> .cal-warning { color: #d63200; font-weight: 600; } </style>这段代码里,dueSoonDevices对设备列表做了第一层过滤,只保留有下次检定日期的设备;isCalDue计算当前日期和检定日期相差天数,落在 0 到 30 天范围内就加红色警示样式。scoped样式是 Vue 单文件组件里最常见的样式隔离方式,让.cal-warning只作用于当前设备的表格单元格,不会影响到报告页里同名的样式。
实际使用的时候,如果检测项目里还包括“设备当前是否在维修中”的字段,在过滤条件里加上d.status === 'ACTIVE'就行。不要在template里写超过三行的复杂表达式,把判断逻辑放到 script 里的函数,后面加“逾期 90 天转成灰色”之类的规则会更快。
3.2 检测任务状态机:用状态管理避免“已完成还能提交”的按钮
检测任务的典型流程是:待接收、已分配、检测中、已完成、已退回。这个状态流转用页面里的v-if控制容易出现漏洞:设备列表页跳过来的用户如果手动改 URL 路径,结果页可能看到和状态不一致的按钮。把状态机放进 store 是更稳的写法。
任务状态对应的操作可以用下面这张表做校验:
| 状态 | 允许执行的操作 | 按钮权重 |
|---|---|---|
| PENDING | 转 ASSIGNED | primary |
| ASSIGNED | 转 IN_PROGRESS、RETURNED | primary / warning |
| IN_PROGRESS | 转 COMPLETED、RETURNED | success / warning |
| RETURNED | 转 ASSIGNED | primary |
| COMPLETED | 无 | disabled |
export const TASK_FLOW = { PENDING: { next: ['ASSIGNED'] }, ASSIGNED: { next: ['IN_PROGRESS', 'RETURNED'] }, IN_PROGRESS: { next: ['COMPLETED', 'RETURNED'] }, RETURNED: { next: ['ASSIGNED'] }, COMPLETED: { next: [] }, }再配合一个触发操作的 action:
async function changeStatus(target) { await api.post(`/tasks/${taskId}/status`, { target }) taskStore.status = target }这里TASK_FLOW用对象字面量描述每个状态允许流转到哪些状态。前端只做提示,真正的状态变更以后端接口返回为准,页面拿到 200 后才修改本地status,所以不会出现接口失败但按钮已经置灰的情况。
任务列表页的每一行操作区,建议用v-for把availableActions渲染成按钮,而不是手写固定两个按钮。这样新增“待复核”状态时,只需要改TASK_FLOW和接口返回逻辑,列表页不用动,这也是源码交付后最常被扩展的一个点。
3.3 报告生成页:A4 打印样式和检测结果字段绑定
检测报告是这个项目的最终交付物,前端的价值体现在预览和打印。报告页直接从路由参数拿到任务号,再请求任务关联的检测项目、设备、样品和结果。
<template> <div class="report-sheet"> <h2>{{ report.taskName }}</h2> <table> <tr><td>检测设备</td><td>{{ report.device.name }}</td></tr> <tr><td>依据标准</td><td>{{ report.standard }}</td></tr> <tr v-for="item in report.items" :key="item.code"> <td>{{ item.name }}</td> <td>{{ item.result }} {{ item.unit }}</td> </tr> </table> </div> </template> <script setup> import { computed } from 'vue' const props = defineProps({ taskId: { type: String, required: true }, }) const report = computed(() => reportStore.getByTaskId(props.taskId)) </script> <style scoped> .report-sheet { width: 210mm; min-height: 297mm; margin: 0 auto; padding: 12mm; font-size: 14px; background: #fff; } .report-sheet table { width: 100%; border-collapse: collapse; } .report-sheet td { border: 1px solid #333; padding: 6px 8px; } </style>这段报告样式用210mm和297mm直接按 A4 纸的物理尺寸来设置,浏览器打印时不会因为屏幕分辨率而改变列宽。v-for渲染检测项目行,保证报告包含的项目顺序和后端返回一致。如果检测报告需要盖电子签章,可以在页面底部放一个img组件,但要跟后端约定签章图片的接口要带防伪参数,不能直接写一个公开的 PNG 地址。
报告页的打印建议用window.print(),然后在全局样式里加上@media print,把侧边导航和顶栏隐藏掉。这里有一个常见做法是把报告做成独立路由,比如/print/:taskId,专门给浏览器打印和 PDF 导出用,避免主布局里的菜单干扰分页。
4. 检测录像回放:Vue 播放 m3u8 视频流的组件封装
医疗器械检测项目里经常需要把内窥镜、显微镜或者操作台录像作为检测过程的留痕。这类录像如果直接传 mp4 文件,文件大、加载慢,遇到网络差的时候回放卡顿。常见的解决方案是把视频切成 ts 切片,用 m3u8 索引文件让前端根据带宽选择码率。在 Vue 项目里播放 m3u8,需要在前端引入一个能解析 HLS 协议的库,最常用的是 hls.js,下面是一个可以直接用的 Vue 组件。
4.1 先区分点播和直播,再决定播放地址怎么传
m3u8 既能做直播也能做点播。检测机构的录像回放绝大多数是点播,也就是说文件已经在设备端或服务端存好,拿到文件索引就能播。这里要把两种来源和对应处理方式分清楚:
| 视频来源 | 地址特征 | 前端处理要点 |
|---|---|---|
| 设备点播回放 | 返回record/m3u8/xxx.m3u8 | 一次性播放地址,可用 hls.js 解码 |
| 内网摄像头直播 | 返回live/stream.m3u8 | 一般需要带鉴权参数,按需加载 |
| 历史转码文件 | 返回video/xxx/index.m3u8 | 路径由后端拼接,前端不拼盘 |
如果是在内网摄像头直播页,地址往往需要带?token=参数,这里要先把完整地址传给播放器,不能只传不带参数的stream.m3u8。直播流断流后 hls.js 会触发bufferStalledError,前端要做自动重试而不是让用户自己刷新页面;点播流则不需要频繁重试,因为任务回放文件不会中途消失。
4.2 封装 InspectionVideo 组件处理 m3u8 播放
新建src/components/InspectionVideo.vue,把播放器的创建和销毁都收在这个文件里。这样任务详情页、报告页、设备录像页共用同一个组件,不用每个页面都写一遍 hls.js 的初始化代码。
<template> <video ref="videoEl" controls playsinline class="inspection-video"></video> </template> <script setup> import Hls from 'hls.js' import { ref, onMounted, onBeforeUnmount, watch } from 'vue' const props = defineProps({ src: { type: String, required: true }, }) const videoEl = ref(null) let hls = null function setupPlayer(url) { if (hls) hls.destroy() if (!url) return if (Hls.isSupported()) { hls = new Hls({ maxBufferLength: 30, manifestLoadingTimeOut: 5000, }) hls.on(Hls.Events.ERROR, (_event, data) => { if (data.fatal && data.type === Hls.ErrorTypes.NETWORK_ERROR) { hls?.startLoad() } }) hls.loadSource(url) hls.attachMedia(videoEl.value) } else if (videoEl.value.canPlayType('application/vnd.apple.mpegurl')) { // Safari 原生支持 m3u8,不走 hls.js videoEl.value.src = url } } watch(() => props.src, (url) => setupPlayer(url)) onMounted(() => setupPlayer(props.src)) onBeforeUnmount(() => hls?.destroy()) </script>组件里几个参数的含义要解释清楚:maxBufferLength控制缓存长度,单位是秒,回放场景设置为 30 秒可以避免频繁请求切片;manifestLoadingTimeOut是 m3u8 索引文件请求的超时时间,单位是毫秒,检测机构内网如果视频服务器响应慢,超过 5 秒会触发网络错误事件,事件里调用hls.startLoad()做一次重试。Hls.isSupported()判断当前浏览器是否支持 MSE,Safari 的特殊分支交给原生播放器处理,避免在 Safari 上出现黑屏。
使用组件时,页面只需要传视频地址:
<InspectionVideo :src="videoUrl" />如果因为 token 过期导致 m3u8 请求返回 401,hls.js 的 error 回调会反复报networkError。这时候不要在前端硬拼 token,正确做法是在初始化的请求阶段把鉴权参数加在 URL 的 query 上,例如stream.m3u8?token=xxx。另外,如果视频服务器没响应跨域限制,hls.js 加载切片时控制台会报跨域错误,这时需要后端在Access-Control-Allow-Origin头里放行前端域名,单纯换一个播放器解决不了问题。
4.3 把回放组件挂到报告页,延迟加载视频
报告页里如果想展示“检测过程视频”,可以在这个位置放一个折叠面板,默认收起,点击“查看检测录像”时才加载视频地址。这样做可以避免报告页面一次性拉多条视频索引,导致列表滚动卡顿。
<el-collapse> <el-collapse-item title="查看检测录像"> <InspectionVideo v-if="videoLoaded" :src="videoUrl" /> <el-button @click="loadVideo">加载录像</el-button> </el-collapse-item> </el-collapse>这里在点击后再设置videoLoaded,组件内部的watch才会触发setupPlayer;如果提前把空的videoUrl传给组件,需要在setupPlayer里增加空字符串的 return,避免组件在mounted阶段报loadSource错误。这个延迟加载的思路也适用于检测报告页其他多媒体资源,比如“现场照片”和“原始数据截图”,统一放在折叠面板里可以明显降低首屏渲染时间。
5. 源码打包验证:解决 Vue 打包后布局异常与 history 路由 404
源码写完之后,进入打包验证。这个阶段最常见的问题有两个:一个是npm run build之后打开页面白屏或者样式错乱,另一个是路由直接访问二级地址变成 404。前者多半是publicPath配置和静态资源路径不对,后者是 vue-router 的 history 模式只配了前端,没有让 Nginx 把不存在的路径回退到index.html。
5.1 打包后布局异常先查这三项
| 现象 | 检查项 | 处理方式 |
|---|---|---|
| 页面白屏,控制台资源 404 | vite.config.js里的base配置 | 部署在子路径时设置base: '/med/',不能留默认/ |
| 样式有,图片和字体丢失 | CSS 里的 url 是否用了绝对路径 | 样式里的资源路径尽量用@/assets引入,不要手写/images |
| 路由跳转正常,刷新后 404 | Nginx location 是否配置 try_files | 配置try_files $uri $uri/ /index.html; |
如果部署到服务器根目录,base可以不用改;但如果系统是挂在一个 Nginx 站点下的med子路径,base不设置,CSS 和 JS 的加载路径就会指向域名根目录,布局必然乱。
5.2 history 路由模式下的 Nginx 配置
使用以下配置处理 SPA 路由回退和静态资源缓存,注意把/med/换成实际部署路径。
location /med/ { alias /opt/med-web/; try_files $uri $uri/ /med/index.html; } location ~* \.(js|css|png|jpg|woff2?)$ { expires 7d; add_header Cache-Control "public, immutable"; }第一段try_files会先找真实文件,找不到就回退到入口 HTML,这样/med/report/T20240601这类带参数的页面在刷新后不会变成 404。第二段对静态资源做缓存,医疗器械检测项目的报告页打开频率很高,js 包有缓存能明显减少重复加载时间。
5.3 交付源码前的三个验证动作
第一步,在本地执行npm run build后,用vite preview启动生产预览,把任务列表、报告详情、视频回放三个页面逐个点开。重点看报告页打印时是否多了菜单栏,如果多了,要检查打印媒体查询是否在全局样式里生效。
第二步,把构建产物放到 Nginx 或者 Tomcat 的静态目录,用两个不同角色的账号登录,确认检测结果报告只能根据当前任务的权限显示,不能直接输入路由地址访问别人的报告。
第三步,检查 m3u8 视频地址是否带有效期。如果视频服务需要签名 URL,前端要在请求视频地址的接口拿到一次性的播放地址,再传给InspectionVideo,不要在源代码里写死视频服务器地址。
最后一个建议是:把vue-tsc或者eslint的校验命令加进npm run build之前,否则多人协同时很容易把未使用的import和类型错误带上生产环境。器械检测项目因为字段多,接口字段名错误在运行时很难暴露,提前做类型检查比等到客户反馈要省事得多。
本文还有配套的精品资源,点击获取