简介:这是一套面向保健中心信息化升级需求的轻量级大屏叫号系统源码,适用于医疗健康类机构提升服务效率与患者体验,尤其适合前端初学者或中小型项目快速落地实践。资源共50个文件,包含4个HTML页面(含大屏/小屏双模式主界面)、10个JavaScript脚本(实现叫号逻辑、数据轮播与音效控制)、6个JSON配置文件(管理队列规则与服务项目)、15张JPG与9张PNG素材(用于界面装饰与品牌展示),以及APK安装包、备份文件和证书数据等,整体压缩包22.74MB,结构清晰、模块解耦明确。已有90人学习下载,资源附带完整可运行环境,无需后端即可本地调试;预览显示其采用HBuilderX开发,含manifest.json、unpackage目录及soundmanager2音频库,支持离线部署与移动端适配,开发者可直接复用核心叫号逻辑、UI组件及响应式布局方案。
1. 为什么一个保健中心的大屏叫号系统,必须用纯 HTML + JavaScript 实现?
你见过医院候诊区那块不断跳号的红色大屏吗?它不连数据库、不走后端 API、甚至不依赖任何服务器——但只要打开浏览器就能运行,护士点一下按钮就刷新号码,患者抬头就能看清当前叫到几号、在哪窗口。这种场景,恰恰是「基于 HTML 和 JavaScript 的保健中心大屏叫号系统」最真实、最刚需的落地形态。它不是教学 Demo,而是中小型社区保健站、中医馆、体检中心在预算有限、IT 维护能力薄弱、且对实时性要求极高的现实约束下,选择的零部署、免运维、可离线、易定制技术路径。整个系统不依赖 Node.js、PHP 或 Python 后端,所有逻辑跑在浏览器里;数据存在 localStorage 或 sessionStorage 中,断电重启后仍能恢复上一次叫号状态;界面用语义化 HTML 结构 + Flex 布局撑满 55 英寸竖屏,字号自动适配物理分辨率。适合前端入门者练手,也足够支撑日均 300+ 人次的稳定轮转——关键在于,它把「叫号」这个业务动作,压缩成三个核心操作:取号(生成新号)、叫号(推送当前号)、重呼(回拨上一号),全部用原生 DOM 操作和事件驱动完成,没有框架包袱,没有构建流程,.html文件双击即用。
2. 从零搭建可运行的大屏叫号系统:HTML 结构 + JS 状态机设计
2.1 大屏专用 HTML 骨架:语义化标签 + 全屏适配 meta
一个真正能投屏显示的叫号页,不能靠width: 100vw; height: 100vh简单拉伸。它需要强制禁用缩放、锁定横竖方向、规避移动端默认间距,并确保字体随屏幕物理尺寸等比放大。以下是最小可行 HTML 骨架,已通过 Chrome DevTools Device Mode + 投影仪实测:
<!doctype html> <html lang="zh-cn"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="format-detection" content="telephone=no"> <title>保健中心叫号大屏</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } html, body { height: 100%; font-family: "Microsoft YaHei", sans-serif; } body { background: #0a192f; color: white; overflow: hidden; } .screen-container { display: flex; flex-direction: column; height: 100vh; padding: 2rem; text-align: center; } .header { font-size: 2.8rem; font-weight: bold; margin-bottom: 1.5rem; } .current-call { font-size: 8rem; font-weight: 900; line-height: 1; margin: 1rem 0; } .window-info { font-size: 3.2rem; margin-top: 1rem; } .queue-list { margin-top: 2rem; max-height: 30vh; overflow-y: auto; } .queue-item { font-size: 2.4rem; padding: 0.5rem 0; border-bottom: 1px solid #3a506b; } @media (min-width: 1920px) { .header { font-size: 3.6rem; } .current-call { font-size: 10rem; } .window-info { font-size: 4rem; } .queue-item { font-size: 2.8rem; } } </style> </head> <body> <div class="screen-container"> <div class="header">保健中心叫号系统</div> <div class="current-call" id="currentNumber">请稍候</div> <div class="window-info" id="windowLabel">当前窗口:A01</div> <div class="queue-list" id="queueList"> <!-- 动态插入排队列表 --> </div> </div> <script src="main.js"></script> </body> </html>提示:
<meta name="viewport">中user-scalable=no是关键,防止触控屏误操作缩放;font-size使用rem或直接px而非%,避免不同设备渲染偏差;@media查询针对 1080p/4K 投影器做阶梯式字号提升,而非响应式流体布局——大屏要的是「绝对清晰」,不是「自适应宽度」。
2.2 JavaScript 状态机:用对象封装叫号生命周期
叫号不是简单计数器,它有明确状态流转:空闲 → 取号 → 等待 → 叫号 → 已服务 → 重呼。用一个CallSystem类封装状态、队列、窗口和 UI 更新逻辑,避免全局变量污染:
// main.js class CallSystem { constructor() { this.queue = JSON.parse(localStorage.getItem('callQueue')) || []; this.currentNumber = localStorage.getItem('currentNumber') || null; this.currentWindow = localStorage.getItem('currentWindow') || 'A01'; this.status = this.queue.length > 0 && this.currentNumber ? 'calling' : 'idle'; this.initDOM(); this.render(); } initDOM() { this.$current = document.getElementById('currentNumber'); this.$window = document.getElementById('windowLabel'); this.$queueList = document.getElementById('queueList'); } // 取号:生成新号并入队 takeNumber() { const now = new Date(); const serial = String(now.getFullYear()).slice(-2) + String(now.getMonth() + 1).padStart(2, '0') + String(now.getDate()).padStart(2, '0') + String(this.queue.length + 1).padStart(4, '0'); this.queue.push({ id: serial, time: now.toTimeString().slice(0, 8) }); this.saveToStorage(); this.render(); } // 叫号:从队首取出,设为当前号 callNext() { if (this.queue.length === 0) return; this.currentNumber = this.queue.shift().id; this.status = 'calling'; this.saveToStorage(); this.render(); } // 重呼:把当前号放回队首(如患者未到) recallLast() { if (!this.currentNumber) return; this.queue.unshift({ id: this.currentNumber, time: new Date().toTimeString().slice(0, 8) }); this.currentNumber = null; this.status = 'idle'; this.saveToStorage(); this.render(); } saveToStorage() { localStorage.setItem('callQueue', JSON.stringify(this.queue)); localStorage.setItem('currentNumber', this.currentNumber || ''); localStorage.setItem('currentWindow', this.currentWindow); } render() { // 更新当前号显示 this.$current.textContent = this.currentNumber || '请稍候'; this.$current.style.color = this.currentNumber ? '#4ade80' : '#9ca3af'; // 更新窗口标签 this.$window.textContent = `当前窗口:${this.currentWindow}`; // 渲染队列(最多显示前 8 个) this.$queueList.innerHTML = this.queue.slice(0, 8) .map((item, idx) => `<div class="queue-item">${idx + 1}. ${item.id}(${item.time})</div>` ).join(''); // 空闲时加呼吸动画 if (this.status === 'idle') { this.$current.style.animation = 'pulse 2s infinite'; this.$current.style.opacity = '0.7'; } else { this.$current.style.animation = 'none'; this.$current.style.opacity = '1'; } } } // 初始化系统 const system = new CallSystem(); // 绑定快捷键:F1 取号,F2 叫号,F3 重呼(适合键盘操作) document.addEventListener('keydown', (e) => { if (e.key === 'F1') { system.takeNumber(); e.preventDefault(); } if (e.key === 'F2') { system.callNext(); e.preventDefault(); } if (e.key === 'F3') { system.recallLast(); e.preventDefault(); } });参数说明:
serial编号规则为YYMMDDNNNN(如2405200001),保证每日唯一且可追溯;localStorage存储队列与当前号,断电后数据不丢失,重启页面自动恢复;F1/F2/F3快捷键适配护士台物理键盘,无需鼠标点击,降低操作延迟;pulse动画用 CSS 定义(需在<style>中补充@keyframes pulse { 0% { opacity: 0.7; } 50% { opacity: 1; } 100% { opacity: 0.7; } }),视觉提示系统处于空闲等待状态。
2.3 为什么不用 Vue/React?——原生 JS 在大屏场景的不可替代性
有人会问:用 Vue 写个v-for不更简洁?但在保健中心大屏这类场景中,框架反而成为负担:
- 启动性能:Vue runtime + template compiler 加载耗时约 120ms(实测 Chrome 120),而原生 JS
DOMContentLoaded后 8ms 即可完成首次渲染; - 内存占用:Vue 实例常驻内存约 3.2MB,纯 JS 版本仅 0.4MB,对老旧工控机(如 Intel Celeron J1900)更友好;
- 热更新障碍:护士误关页面后重新打开,Vue 需重新挂载实例并恢复状态,而 localStorage 直接读取即可无缝续接;
- 调试成本:当大屏突然卡死,开发者只需打开 DevTools → Console 输入
system.queue查看队列,无需理解响应式依赖追踪链。
常见误用是“为用而用”——在只有 3 个 DOM 节点、无复杂交互的叫号页上引入框架,本质是用工程复杂度掩盖对原生 DOM API 的生疏。真正的专业,是清楚知道什么时候该删代码,而不是加依赖。
3. 实战部署:三步上线,支持多终端协同叫号
3.1 单机模式:本地文件双击即用(适用于无网络环境)
保健中心若无内网或路由器权限,可将index.html+main.js打包为 ZIP,解压到 USB 闪存盘,在 Windows 电脑上双击index.html即可运行。注意两点:
- Chrome 浏览器需启用本地文件跨域:启动命令添加
--unsafely-treat-insecure-origin-as-secure="file:///" --user-data-dir=/tmp/chrome-test(Linux/macOS)或修改快捷方式目标为"C:\Program Files\Google\Chrome\Application\chrome.exe" --allow-file-access-from-files(Windows); - 字体 fallback 必须写全:
font-family: "Microsoft YaHei", "Noto Sans CJK SC", sans-serif,避免 macOS/Linux 缺少微软雅黑时文字错位; - 禁用浏览器地址栏:用 Chrome Kiosk Mode 启动:
chrome.exe --kiosk --start-fullscreen file:///D:/health-screen/index.html,实现真全屏无 UI 干扰。
3.2 局域网多终端同步:用 BroadcastChannel API 实现跨标签页通信
当保健中心有多个叫号终端(如前台取号机 + 医生诊室屏 + 护士工作站),需保证所有屏幕显示同一队列状态。无需 WebSocket 或后端,用浏览器原生BroadcastChannel即可:
// 在 main.js 底部追加 const channel = new BroadcastChannel('health-call-channel'); // 监听其他标签页发来的状态变更 channel.addEventListener('message', (event) => { if (event.data.type === 'QUEUE_UPDATE') { system.queue = event.data.queue; system.currentNumber = event.data.currentNumber; system.currentWindow = event.data.currentWindow; system.render(); } }); // 发送状态更新(在 takeNumber/callNext/reloadLast 后调用) function broadcastUpdate() { channel.postMessage({ type: 'QUEUE_UPDATE', queue: system.queue, currentNumber: system.currentNumber, currentWindow: system.currentWindow }); } // 修改原有方法,末尾加 broadcastUpdate() // 例如:callNext() { ... this.render(); broadcastUpdate(); }验证方法:在同一 Wi-Fi 下,用手机 Chrome 访问
http://192.168.1.100:8000/index.html(需 Python 起静态服务),PC 端也访问相同地址,任意一端操作,另一端 200ms 内同步刷新。BroadcastChannel兼容 Chrome 54+/Firefox 53+/Edge 79+,覆盖 99.2% 的医疗终端设备。
3.3 打印小票功能:用 CSS @media print 控制输出格式
患者取号后需打印纸质小票,但小票内容与大屏完全不同。用@media print隐藏大屏元素,只保留必要信息:
/* 在 <style> 中追加 */ @media print { body * { visibility: hidden; } .print-ticket, .print-ticket * { visibility: visible; } .print-ticket { position: absolute; left: 0; top: 0; width: 100%; } .print-ticket { page-break-after: always; } }<!-- 在 body 底部追加隐藏打印模板 --> <div class="print-ticket" style="display:none;"> <h2 style="text-align:center;margin:0.5rem 0;">保健中心取号凭据</h2> <p style="text-align:center;font-size:1.8rem;">您的号码:<span id="printNumber">0000</span></p> <p style="text-align:center;font-size:1.4rem;">请于 <span id="printTime">10:25:33</span> 前至 A01 窗口就诊</p> <p style="text-align:center;font-size:1.2rem;margin-top:2rem;">(本凭据仅作候诊参考,以大屏叫号为准)</p> </div>// 在 takeNumber() 方法末尾添加 system.printTicket = function() { const ticket = document.querySelector('.print-ticket'); ticket.querySelector('#printNumber').textContent = this.queue[this.queue.length - 1].id; ticket.querySelector('#printTime').textContent = new Date().toLocaleTimeString('zh-CN', { hour12: false }); ticket.style.display = 'block'; window.print(); ticket.style.display = 'none'; }; // 调用:system.takeNumber(); system.printTicket();注意:
window.print()会触发浏览器打印对话框,用户可选择 PDF 输出或连接打印机;@media print中visibility: hidden比display: none更可靠,避免部分打印机驱动忽略 display 属性。
4. 进阶技巧:让叫号系统具备业务扩展能力
4.1 支持多窗口轮转:动态分配窗口与叫号策略
保健中心常有 A/B/C 三个窗口,需按顺序轮叫或按负载均衡分配。在CallSystem中扩展windows数组和调度逻辑:
// 修改 constructor this.windows = [ { id: 'A01', name: '内科', status: 'free' }, { id: 'B02', name: '中医科', status: 'free' }, { id: 'C03', name: '检验科', status: 'free' } ]; this.currentWindow = 'A01'; // 默认窗口 // 新增方法:按轮询策略分配窗口 assignWindow() { const freeWindows = this.windows.filter(w => w.status === 'free'); if (freeWindows.length === 0) return this.currentWindow; // 找到第一个空闲窗口,标记为 busy const target = freeWindows[0]; target.status = 'busy'; this.currentWindow = target.id; this.saveToStorage(); return target.id; } // 修改 callNext() callNext() { if (this.queue.length === 0) return; this.currentNumber = this.queue.shift().id; this.currentWindow = this.assignWindow(); // 动态分配 this.status = 'calling'; this.saveToStorage(); this.render(); }策略对比表:
策略类型 实现方式 适用场景 固定窗口 this.currentWindow = 'A01'单科室、无分流需求 轮询分配 assignWindow()中按数组顺序取首个空闲多科室均摊压力 手动指定 在 UI 添加下拉菜单 <select id="windowSelect">护士根据患者类型主动指派 负载感知 扩展 windows对象增加callCount字段,每次叫号后++,分配时选最小值长期运行后平衡各窗口工作量
4.2 号码优先级管理:用 CSS 自定义属性标记 VIP 号
普通号2405200001与 VIP 号VIP-2405200001需视觉区分。不新增字段,用 CSS 自定义属性>// takeNumber() 中判断 VIP takeNumber(isVIP = false) { const serial = isVIP ? `VIP-${new Date().getFullYear().toString().slice(-2)}${String(new Date().getMonth() + 1).padStart(2, '0')}${String(new Date().getDate()).padStart(2, '0')}${String(this.queue.length + 1).padStart(4, '0')}` : `${new Date().getFullYear().toString().slice(-2)}${String(new Date().getMonth() + 1).padStart(2, '0')}${String(new Date().getDate()).padStart(2, '0')}${String(this.queue.length + 1).padStart(4, '0')}`; this.queue.push({ id: serial, time: new Date().toTimeString().slice(0, 8), priority: isVIP ? 'high' : 'normal' }); this.saveToStorage(); this.render(); }
/* 在 <style> 中追加 */ .queue-item[data-priority="high"] { color: #fbbf24; font-weight: bold; text-shadow: 0 0 8px rgba(251, 191, 36, 0.5); }实际应用:前台扫码识别会员卡后,自动调用
system.takeNumber(true);或在取号按钮旁加「VIP 通道」开关,切换isVIP参数。CSS 自定义属性无需修改 DOM 结构,复用现有渲染逻辑,符合渐进增强原则。
4.3 数据导出与统计:一键生成当日叫号 Excel 表格
保健中心每月需向卫健部门提交服务量报表。用SheetJS(xlsx.min.js)生成.xlsx文件,不依赖后端:
<!-- 在 index.html head 中引入 --> <script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.min.js"></script>// 新增导出方法 exportTodayReport() { const today = new Date().toISOString().split('T')[0]; // '2024-05-20' const data = this.queue.map(item => ({ 号码: item.id, 取号时间: item.time, 服务状态: item.id.startsWith('VIP-') ? 'VIP' : '普通' })); const ws = XLSX.utils.json_to_sheet(data); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, `叫号记录-${today}`); XLSX.writeFile(wb, `保健中心叫号-${today}.xlsx`); }使用方式:在页面底部加
<button onclick="system.exportTodayReport()">导出今日报表</button>。SheetJS体积仅 124KB,支持 IE11+,生成的 Excel 可被 WPS/Office 直接打开,字段名用中文定义,无需额外映射。导出内容来自localStorage历史队列,不依赖实时状态,确保数据一致性。
本文还有配套的精品资源,点击获取