简介:基于Bootstrap与jQuery构建的医院大数据可视化管理前端,面向医疗信息化开发者、前端工程师及数据展示学习者,提供可复用的运营数据看板与交互查询方案。压缩包共79个文件,以40个png界面素材、12个js逻辑脚本、5个html页面、5个css样式为主,配合woff/ttf/svg等字体图标资源,整体仅1.4MB,轻量易部署。页面覆盖NCD慢性病、配额、趋势、综合指标等看板场景,借助Ajax异步加载、动态筛选排序、模态框及Bootstrap栅格布局,实现多终端自适应展示;源码目录清晰划分styles、scripts、views、fonts模块,便于定位和二次开发。已有149人学习使用,适合需要快速搭建医院运营数据展示原型,或希望研读Bootstrap+jQuery数据交互、响应式布局与图表联动实现的开发者。
1. 医院大数据展示项目的真实起点:bootstrap+jquery 的快速表达
拿到一个bootstrap+jquery医院大数据展示源码.rar,大概率不是来看架构的,而是要在明天上班前把一块医院运营大屏立起来。这类源码通常是一个纯静态页面集合:bootstrap 负责栅格和组件,jquery 负责 DOM 操作和数据请求,再配一个 ECharts 撑起图表主体。它不需要 node 环境,解压后放进 Web 容器就能跑,对信息科、HIS 服务商或者做医疗信息相关毕设的工程师来说,这套组合仍然是最低成本的交付路径。我一般会先按“布局到组件、图表到刷新、数据到联动”的顺序把它拆开,确认哪些能直接用,哪些要改参数才能对接自己的数据源。本文不介绍某个具体项目的细节,只讲这一类源码背后的通用解法。
2. 用 bootstrap 栅格拆解医院大屏的页面面板
2.1 先画格子:12 列栅格怎么分配给医院大屏
医院大数据展示页面最常见的结构是:顶部一排指标卡,中部左右各一块图表面板,中间留一块面积最大的趋势图,底部偶尔放排队时长或床位周转率。这张版图直接用 bootstrap 的栅格系统铺出来,比手写 flex 在响应式上更稳,尤其当展示终端可能是 1920 宽的竖屏一体机或 16:9 的液晶拼接屏时。
我习惯以row为横向容器,把指标卡和图表面板都拆成col-xl-*的列。注意 bootstrap 默认只有 12 列,如果源码里出现col-lg-3 col-xl-6这样的组合,意思是小于 1200px 时占四分之一宽,大于 1200px 时占二分之一宽。大屏分辨率通常在 1920 以上,所以真正起作用的是col-xl这一档:
<div class="container-fluid"> <div class="row" id="metricRow"> <div class="col-xl-3 col-lg-6">今日门急诊量</div> <div class="col-xl-3 col-lg-6">在院人数</div> <div class="col-xl-3 col-lg-6">手术台次</div> <div class="col-xl-3 col-lg-6">床位使用率</div> </div> <div class="row"> <div class="col-xl-4">科室挂号占比</div> <div class="col-xl-8">近 30 日门急诊趋势</div> </div> </div>这段布局里,container-fluid去掉了固定宽度限制,让页面铺满整个大屏;col-xl-4和col-xl-8用来控制左右面板的宽窄比。若想中间主图再突出一些,可以把比率改成col-xl-3与col-xl-9。栅格只解决横向分配,纵向高度必须由内部图表容器决定,这里不要试图用row等分高度,后面会专门讲容器高度的问题。
2.2 指标卡和下拉筛选的 bootstrap 基础组件选择
大屏顶部指标卡不是一个单独的 bootstrap 组件,常规做法是用jumbotron、card或纯自定义div组合。考虑到 rar 里的源码往往兼容老版本,我建议优先使用.card,因为它自带边框、内边距和阴影,在深色大屏风格下只需覆写背景色即可。
下拉筛选则几乎是所有医院大屏页面都会遇到的功能。科室筛选、时间粒度切换、传染病分类过滤,这些交互用 bootstrap 的dropdown组件加 jquery 驱动。关键在于老版本 bootstrap 的下拉菜单是依赖 jquery 的,这也是标题里把 jquery 和 bootstrap 并列的原因。bootstrap 3 和 bootstrap 4 使用><div class="btn-group"> <button class="btn btn-primary dropdown-toggle" type="button">html, body { height: 100%; margin: 0; overflow: hidden; } .metric-panel { height: 120px; } .chart-wrapper { height: calc(100vh - 220px); } .chart-box { width: 100%; height: 100%; }
这里100vh是视口高度,减去顶部指标卡和标题栏的高度后,剩余空间全部交给图表区。如果不加这一层,ECharts 会经常出现“容器高度为 0,图形画不出来”的问题。遇到这种问题不要先去调 ECharts,先检查父级是否有确定高度。医院大屏切换分辨率后图表不居中,多半也是这个原因,这属于布局层的缓存失效,而不是图表库本身的 bug。
作者在源码头文件里常会给body设min-width: 1280px或更大,防止在小屏上被挤压。这种做法在固定终端上没问题,但如果你要临时用笔记本演示,最好改成基于calc的等比缩放方案,否则横向滚动条会逼着你在演示现场手动改分辨率。
3. 用 ECharts 和 jquery 完成图表渲染与定时刷新
3.1 页面初始化时 ECharts 实例的正确写法
这类大屏源码的核心图表层几乎都交给 ECharts。用 jquery 初始化 ECharts 有一个常见误区:直接把 DOM 对象传给echarts.init(),但 jquery 对象不能直接用,必须先取原生元素。正确写法是:
$(function () { var chartDom = document.getElementById('mainTrend'); var myChart = echarts.init(chartDom); var option = { tooltip: { trigger: 'axis' }, xAxis: { type: 'category', data: ['周一', '周二', '周三'] }, yAxis: { type: 'value', name: '人次' }, series: [{ name: '门急诊量', type: 'line', smooth: true, data: [1200, 1320, 1410] }] }; myChart.setOption(option); });echarts.init第二个参数还能传主题,深色大屏通常使用dark主题或者自定义注册主题。如果要动态加载主题文件,记得在脚本引入顺序上把它放在 echarts 主文件之后,否则会报找不到注册主题。图表一旦初始化完成,后续更新数据不需要重新init,只需再次setOption,否则每次刷新都会重建整个图表,消耗内存且画面闪烁。
3.2 定时刷新与页面可见性:大屏不是无脑 setInterval
医院大屏的数据需要不断更新,但更新频率不能一刀切。十个并发病房数据可以一分钟刷一次,门急诊量和床位状态这类指标五分钟刷一次也合理。直接对所有图表统一setInterval会造成请求风暴,尤其是后端接口未经缓存时,容易把 HIS 的数据库连接池打满。
我的做法是把所有图表的刷新函数收集到一个refresher容器里,由统一控制器调度:
var refresher = { timers: [], add: function (fn, interval) { fn(); this.timers.push(setInterval(fn, interval)); }, stopAll: function () { this.timers.forEach(clearInterval); this.timers = []; } }; refresher.add(function () { $.getJSON('/api/outpatient/trend', function (data) { trendChart.setOption({ series: [{ data: data.series }] }); }); }, 60000);refresher.add里先执行一次fn(),是为了避免页面打开后要等一个刷新周期才有数据。停止所有定时器的方法在单页应用切换路由或页面隐藏时非常有用。还有一个容易漏的细节:浏览器后台标签页的定时器会降频,当从后台切回大屏页面时,图表可能有一大段时间没更新。结合visibilitychange事件做一次立即刷新是比较稳妥的做法:
document.addEventListener('visibilitychange', function () { if (!document.hidden) { trendChart.resize(); refreshOutpatientData(); } });3.3 大屏自带 resize 机制的实现
拼接屏或窗口模式切换时,ECharts 不会自动适应容器大小,必须调用resize()。多数源码只在window.resize里做一次,这在普通网页里足够,但大屏页面如果包含侧边栏折叠操作,图表容器宽度变化而 window 尺寸没变,resize就不会触发。更可靠的做法是用封装好的自适应函数统一挂到window.onresize上,同时给每个图表实例做好防抖。
var resizeTimer; window.addEventListener('resize', function () { clearTimeout(resizeTimer); resizeTimer = setTimeout(function () { trendChart.resize(); pieChart.resize(); deptChart.resize(); }, 200); });200 毫秒的防抖延迟是为了避免拼接屏切换分辨率时连续触发几十次resize导致主线程阻塞。如果图表数量多到上百个,逐个resize仍然会卡,此时应只针对可视区域内的图表容器调用resize。不过医院大屏一般三到六张图,上面的写法已经够用。js 报错集中出现在这里时,多半是某个图表实例在隐藏的 tab 页里初始化失败,echarts.getInstanceByDom()返回了空对象,调用前最好做一个非空判断。
4. 医院大屏的数据接入:ajax、mock 数据与下拉联动
4.1 没有后端接口时,先用 mock 数据把页面撑起来
一份能直接交付的医院大数据展示源码,一般不会把真实接口写在页面里,而是用 mock 数据文件代替,方便没有后端时先看效果。常见做法是在项目根目录放一个data/目录,里面是若干个 json 文件,页面通过$.getJSON读本地文件。这里要特别注意:直接双击 html 用file://协议打开时,浏览器会拦截 ajax 请求,医院环境里最常见的报错就是Cross origin requests are only supported for protocol schemes。源码虽然能解压,但不代表直接双击就能看,必须用 nginx、python -m http.server或任意静态服务容器挂在 http 协议下访问。
mock 数据文件的结构要贴近真实接口。下面是一份门急诊趋势数据的示例:
{ "dateRange": ["2025-05-01", "2025-05-30"], "unit": "人次", "series": [1800, 1930, 2015, 2210, 2080, 2330], "deptRank": [ { "dept": "呼吸科", "count": 3421 }, { "dept": "心内科", "count": 2891 } ] }字段命名建议与后端约定保持完全一致,因为后续接真实接口时,前端大概率只替换接口地址而不改解析逻辑。deptRank这类排行榜数据尤其要保证数组顺序按数值倒序,因为图表侧不一定负责排序。
字段名、类型和含义需要一份内部核对表,每次对接接口时按表排查比逐行读代码快得多。下面列的是医院大屏里最常见的三类字段:
| 字段名 | 类型 | 含义 | 常见误区 |
|---|---|---|---|
| dept | string | 科室名称 | 前后端命名不一致,如 dept vs department |
| count | number | 数量 | 被当作字符串返回,图表会显示 NaN |
| timestamp | string | 统计时间 | 未做时区处理,跨院区时偏差 1 小时 |
4.2 下拉筛选与数据刷新联动:change 事件绑定的典型误区
下拉菜单选中某个科室后,图表和指标卡要同步更新,这是医院大屏最核心的交互逻辑。bootstrap 的dropdown-menu里的每一项点击后,需要自己把选中的科室名记录下来,再重新请求接口。一种常见做法是给每个li a挂单独click事件,但页面里如果动态追加下拉项,这种写法就失效了,必须换成事件委托:
$(document).on('click', '#deptMenu a[data-value]', function (e) { e.preventDefault(); var dept = $(this).data('value'); $('#deptLabel').text(dept); $.getJSON('/api/outpatient/month', { dept: dept }, function (data) { trendChart.setOption({ xAxis: { data: data.dateRange }, series: [{ data: data.series }] }); }); });这里用$(document).on(...)把事件绑定在document上,即使#deptMenu里的列表项被整体替换也能捕获到点击。取所选值时用$(this).data('value')而不是attr('data-value'),前者会自动做类型转换,数字字符串会变成数字,这对后续接口参数拼接更友好。点击下拉项后记得调用e.preventDefault(),否则a标签的默认跳转会刷新页面,刚拉到的数据瞬间丢失。
还有一个经常被忽略的问题:下拉筛选触发多次请求时,上一次未完成的请求可能先回来,把新一次的结果覆盖掉。解决方式是给 jquery 的ajaxSetup配置一个递增序列号,或者在$.getJSON回调里判断发起时的时间戳是否为最新。简单场景下可以这样处理:
var reqSeq = 0; function fetchByDept(dept) { var currentSeq = ++reqSeq; $.getJSON('/api/outpatient/month', { dept: dept }, function (data) { if (currentSeq !== reqSeq) return; trendChart.setOption({ series: [{ data: data.series }] }); }); }4.3 表格、列表的动态渲染与 jquery 选择器性能
大屏左下角通常有“科室挂号量排行”或“药占比列表”。这类列表用 jquery 渲染最直接,但不建议用字符串拼接后一次性append,更不要在每个tr里写onclick。我会先用数组map出 HTML,再一次性插入到 tbody,这样减少 DOM 操作次数:
var rowsHtml = data.deptRank.map(function (item) { return '<tr><td>' + item.dept + '</td><td>' + item.count + '</td></tr>'; }).join(''); $('#rankBody').html(rowsHtml);这些th里的顺序和接口返回字段顺序要保持一致,一旦后端字段名变化,表格列头和数据对不上是必然事件,排查时先看浏览器 Network 面板返回的实际 json,再对照表头检查。在数据量大、需要刷新频繁的页面上,$('#rankBody').html(...)比逐行.append()更划算,因为前者只触发一次回流,后者每 append 一行都会触发一次。
5. 解压后立即能跑的验证顺序:起服务、看图表、压内存
5.1 在本地起一个静态服务代替直接打开 html
拿到 rar 解压后的目录,先不要急着进index.html,用一条命令起一个静态服务。最简单的方案是用 python 自带的 http 模块,路径切到项目目录下即可:
cd hospital-dashboard python -m http.server 8080然后访问http://localhost:8080。如果机器上没装 python,可以用 nginx 在conf/nginx.conf里把root指向解压目录,或者用 npxhttp-server。这一步只为了解决两个问题:一是绕开file://禁止跨域读取 json 的限制,二是确保页面里的 HTTP 请求走相对路径正常解析。我见过不少源码压缩包在写接口时用的是绝对路径/api/...,直接双击打开时路径解析到磁盘根目录,页面一片空白,但 Network 面板里什么都没有,起服务后立刻恢复正常。
5.2 用 Performance 面板验证主线程占用和定时器膨胀
大屏页面一般会长时间挂在电视或拼接墙上,内存泄漏最典型的表现是运行两三天后页面变得卡顿。打开 Chrome DevTools 的 Performance 面板,点录制,操作一遍科室下拉切换、时间粒度切换、页面切后台再切回,观察 Main 线程的红色长任务是否出现在每个setInterval触发的瞬间附近。如果红色任务多且长,优先检查每个图表是否在setOption时传入了完整的新 option,而不是只传变化的字段。完整重建 option 会让 ECharts 内部保留大量旧数据,内存占用随刷新次数线性增长。医院大屏的图表数据最多几万个点,远没有到需要 WebGL 渲染的地步,但这个泄漏点非常普遍。
5.3 压缩交付前的三项核对:图表容器高度、下拉事件、接口地址
源码准备重新打包成 rar 交付时,我通常按下面的顺序做最后一轮检查。先输入一段脚本,确认所有图表容器都有非零高度:
document.querySelectorAll('.chart-box').forEach(function (el) { if (el.clientHeight === 0) { console.warn('height 0:', el); } });再检查下拉菜单的style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />