| | |
| | | <!DOCTYPE html> |
| | | <!DOCTYPE html> |
| | | <html lang="zh-CN"> |
| | | |
| | | <head> |
| | |
| | | .header-top { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | gap: 30px; |
| | | justify-content: space-between; |
| | | } |
| | | |
| | | .logo { |
| | |
| | | display: flex; |
| | | flex-direction: column; |
| | | line-height: 1.2; |
| | | text-align: center; |
| | | text-align: right; |
| | | } |
| | | |
| | | .week-day, |
| | | .full-date { |
| | | font-size: 16px; |
| | | color: #666; |
| | | } |
| | | |
| | | .header-right { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 15px; |
| | | } |
| | | |
| | | .clock { |
| | |
| | | border-radius: 12px; |
| | | display: flex; |
| | | flex-direction: row; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); |
| | | box-shadow: 0 3px 12px rgba(0, 0, 0, 0.15); |
| | | overflow: hidden; |
| | | flex: 1; |
| | | border: 2px solid #e0e0e0; |
| | | } |
| | | |
| | | .status-active { |
| | | border-color: #67c23a; |
| | | } |
| | | |
| | | .status-waiting { |
| | | border-color: #e6a23c; |
| | | } |
| | | |
| | | .status-missed { |
| | | border-color: #f56c6c; |
| | | } |
| | | |
| | | /* 左侧状态标签 (竖排) */ |
| | | .panel-header { |
| | | width: 60px; |
| | | width: 70px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | font-size: 22px; |
| | | font-size: 32px; |
| | | font-weight: bold; |
| | | color: #fff; |
| | | writing-mode: vertical-rl; |
| | | letter-spacing: 6px; |
| | | letter-spacing: 12px; |
| | | text-align: center; |
| | | flex-shrink: 0; |
| | | } |
| | |
| | | /* 患者信息项 (固定高度,一行两个) */ |
| | | .patient-item { |
| | | width: calc(50% - 5px); |
| | | height: 60px; |
| | | height: 70px; |
| | | display: flex; |
| | | justify-content: space-between; |
| | | justify-content: flex-start; |
| | | align-items: center; |
| | | padding: 0 15px; |
| | | padding: 0 8px; |
| | | background: #ffffff; |
| | | border: 1px solid #eee; |
| | | border-radius: 8px; |
| | | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); |
| | | white-space: nowrap; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .p-name { |
| | | font-size: 28px; |
| | | font-size: 36px; |
| | | font-weight: bold; |
| | | color: #303133; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .p-num { |
| | | font-size: 24px; |
| | | font-size: 30px; |
| | | color: #909399; |
| | | font-weight: bold; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | /* ================= 底部控制区 ================= */ |
| | |
| | | } |
| | | |
| | | #test-voice-btn { |
| | | display: none; |
| | | position: absolute; |
| | | right: 20px; |
| | | background-color: #007bff; |
| | |
| | | |
| | | /* 调试信息区 */ |
| | | .debug-info { |
| | | display: none; |
| | | font-size: 14px; |
| | | color: #999; |
| | | background: #f0f0f0; |
| | |
| | | <div class="header"> |
| | | <div class="header-top"> |
| | | <img src="logo.png" alt="Logo" class="logo"> |
| | | <div class="time-box"> |
| | | <span class="week-day" id="weekDay">星期日</span> |
| | | <span class="full-date" id="fullDate">2024年01月01日</span> |
| | | <div class="header-right"> |
| | | <div class="time-box"> |
| | | <span class="week-day" id="weekDay">星期日</span> |
| | | <span class="full-date" id="fullDate">2024年01月01日</span> |
| | | </div> |
| | | <div class="clock" id="clock">12:00</div> |
| | | </div> |
| | | <div class="clock" id="clock">12:00</div> |
| | | </div> |
| | | <div class="room-line"><span id="currentRoomId">--</span></div> |
| | | </div> |
| | |
| | | <!-- 主体内容 --> |
| | | <div class="main-container"> |
| | | <!-- 1. 正在就诊 --> |
| | | <div class="panel status-active" style="max-height: calc(60px * 2 + 20px + 20px);"> |
| | | <div class="panel-header">正在就诊</div> |
| | | <div class="panel status-active" style="max-height: calc(70px * 2 + 20px + 20px);"> |
| | | <div class="panel-header">诊中</div> |
| | | <div class="list-content" id="inProgressList"></div> |
| | | </div> |
| | | |
| | | <!-- 2. 候诊中 --> |
| | | <div class="panel status-waiting" style="max-height: calc(60px * 4 + 30px + 20px);"> |
| | | <div class="panel-header">候诊中</div> |
| | | <div class="panel status-waiting" style="max-height: calc(70px * 4 + 30px + 20px);"> |
| | | <div class="panel-header">等候</div> |
| | | <div class="list-content" id="waitingList"></div> |
| | | </div> |
| | | |
| | | <!-- 3. 过号 --> |
| | | <div class="panel status-missed" style="max-height: calc(60px * 3 + 20px + 20px);"> |
| | | <div class="panel status-missed" style="max-height: calc(70px * 3 + 20px + 20px);"> |
| | | <div class="panel-header">过号</div> |
| | | <div class="list-content" id="missedList"></div> |
| | | </div> |
| | |
| | | |
| | | <!-- 底部控制栏 --> |
| | | <div class="footer"> |
| | | <span class="footer-text">温馨提示:请耐心等待,保持安静!</span> |
| | | <span class="footer-text">温馨提示:请过号患者到分诊台处理!</span> |
| | | <button id="test-voice-btn">测试语音</button> |
| | | </div> |
| | | |
| | |
| | | <div class="debug-info" id="debugInfo">调试状态:等待数据...</div> |
| | | |
| | | <script> |
| | | // ================= URL 参数读取 ================= |
| | | function getUrlParam(name) { |
| | | var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i'); |
| | | var r = window.location.search.substr(1).match(reg); |
| | | if (r != null) return decodeURIComponent(r[2]); |
| | | return null; |
| | | } |
| | | |
| | | // ================= 配置参数 ================= |
| | | var CONFIG = { |
| | | apiBaseUrl: "http://192.168.3.12:48080/admin-api", |
| | | // apiBaseUrl: "http://192.168.100.110:48080/admin-api", |
| | | roomId: "116", |
| | | // apiBaseUrl: "http://192.168.3.12/admin-api", |
| | | apiBaseUrl: "http://10.0.2.193/admin-api", |
| | | roomId: getUrlParam("roomID") || "116", |
| | | refreshRate: 5000 |
| | | }; |
| | | var CALL_TIMES = 2; // 叫号次数 |
| | | // 诊室编号 → 名称映射:当接口未返回 roomName 时兜底 |
| | | var ROOM_NAME_MAP = { |
| | | "116": "1号诊室", |
| | | "117": "2号诊室", |
| | | "118": "3号诊室", |
| | | "119": "4号诊室", |
| | | "121": "6号诊室", |
| | | "123": "8号诊室", |
| | | "125": "分诊台" |
| | | }; |
| | | var CALL_TIMES = 2; // 叫号次数 |
| | | |
| | | var appState = { roomName: '', lastSpokenPatient: null }; |
| | | var appState = { roomName: '', lastSpokenPatient: null, serverTimeOffset: 0 }; |
| | | function $(id) { return document.getElementById(id); } |
| | | |
| | | // ================= 时间模块 ================= |
| | | function getNow() { |
| | | return new Date(Date.now() + (appState.serverTimeOffset || 0)); |
| | | } |
| | | function updateClock() { |
| | | var now = new Date(); |
| | | var now = getNow(); |
| | | $('clock').innerText = ('0' + now.getHours()).slice(-2) + ':' + ('0' + now.getMinutes()).slice(-2); |
| | | $('fullDate').innerText = now.getFullYear() + '年' + ('0' + (now.getMonth() + 1)).slice(-2) + '月' + ('0' + now.getDate()).slice(-2) + '日'; |
| | | $('weekDay').innerText = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'][now.getDay()]; |
| | | } |
| | | function syncServerTime(serverTimeStr) { |
| | | if (!serverTimeStr) return; |
| | | try { |
| | | var serverTime = new Date(serverTimeStr); |
| | | if (!isNaN(serverTime.getTime())) { |
| | | appState.serverTimeOffset = serverTime.getTime() - Date.now(); |
| | | updateDebugInfo("时间已同步 | 偏移=" + (appState.serverTimeOffset / 1000).toFixed(1) + "s"); |
| | | } |
| | | } catch (e) { updateDebugInfo("时间同步失败: " + e.message); } |
| | | } |
| | | setInterval(updateClock, 1000); updateClock(); |
| | | |
| | | // ================= 语音播报 ================= |
| | | var _gUtterance = null; // GC 保护 |
| | | function speakText(text, times) { |
| | | times = times || CALL_TIMES; |
| | | var isAndroid = /Android/i.test(navigator.userAgent); |
| | | if (isAndroid && window.wowjoy && typeof window.wowjoy.speek === 'function') { |
| | | for (var i = 0; i < times; i++) setTimeout(function () { window.wowjoy.speek(text); }, i * 1500); |
| | | updateDebugInfo("TTS: wowjoy模式"); |
| | | for (var i = 0; i < times; i++) { |
| | | setTimeout(function () { window.wowjoy.speek(text); }, i * 1500); |
| | | } |
| | | } else if (window.speechSynthesis) { |
| | | window.speechSynthesis.cancel(); |
| | | var utterance = new window.SpeechSynthesisUtterance(text); |
| | | utterance.lang = 'zh-CN'; utterance.rate = 1.0; |
| | | window.speechSynthesis.speak(utterance); |
| | | for (var j = 1; j < times; j++) setTimeout(function () { window.speechSynthesis.speak(utterance); }, j * 1500); |
| | | updateDebugInfo("TTS: speechSynthesis模式"); |
| | | if (window.speechSynthesis.speaking) { |
| | | window.speechSynthesis.cancel(); |
| | | } |
| | | function doSpeak(idx) { |
| | | var u = new window.SpeechSynthesisUtterance(text); |
| | | u.lang = 'zh-CN'; |
| | | u.rate = 1.0; |
| | | u.volume = 1.0; |
| | | _gUtterance = u; |
| | | u.onend = function () { _gUtterance = null; }; |
| | | u.onerror = function (e) { _gUtterance = null; updateDebugInfo("TTS错误: " + (e.error || "unknown")); }; |
| | | window.speechSynthesis.speak(u); |
| | | } |
| | | doSpeak(0); |
| | | for (var j = 1; j < times; j++) { |
| | | setTimeout((function (idx) { return function () { doSpeak(idx); }; })(j), j * 1500); |
| | | } |
| | | } else { |
| | | updateDebugInfo("TTS: 无引擎可用"); |
| | | } |
| | | } |
| | | |
| | | // ================= 数据请求 ================= |
| | | function fetchData() { |
| | | var url = CONFIG.apiBaseUrl + "/ecg/screen/room-screen-data?roomId=" + CONFIG.roomId; |
| | | updateDebugInfo("请求: " + url); |
| | | var xhr = new XMLHttpRequest(); |
| | | xhr.open('GET', url, true); |
| | | xhr.timeout = 8000; |
| | | xhr.onreadystatechange = function () { |
| | | if (xhr.readyState === 4) { |
| | | if (xhr.status === 200) { |
| | | try { |
| | | var response = JSON.parse(xhr.responseText); |
| | | processData(response); |
| | | updateDebugInfo("获取数据成功 | 原始数据长度: " + (response.data ? Object.keys(response.data).length : 0)); |
| | | } catch (e) { updateDebugInfo("JSON解析失败: " + e.message); } |
| | | } else { updateDebugInfo("请求失败,状态码: " + xhr.status); } |
| | | updateDebugInfo("成功 | 患者: " + (response.data ? JSON.stringify(Object.keys(response.data)) : "无")); |
| | | } catch (e) { updateDebugInfo("处理失败: " + e.message); } |
| | | } else if (xhr.status === 0) { |
| | | updateDebugInfo("网络错误(status=0) | " + url + " | 请检查API服务/URL可达性/跨域"); |
| | | } else { |
| | | updateDebugInfo("请求失败 | status=" + xhr.status + " | " + url); |
| | | } |
| | | } |
| | | }; |
| | | xhr.send(); |
| | | xhr.onerror = function () { |
| | | updateDebugInfo("网络异常(onerror) | " + url + " | 设备可能无法访问该地址"); |
| | | }; |
| | | xhr.ontimeout = function () { |
| | | updateDebugInfo("请求超时 | " + url); |
| | | }; |
| | | try { |
| | | xhr.send(); |
| | | } catch (e) { |
| | | updateDebugInfo("send异常: " + e.message); |
| | | } |
| | | } |
| | | |
| | | // ================= 核心业务逻辑处理 ================= |
| | | function processData(res) { |
| | | syncServerTime(res.serverTime || (res.data && res.data.serverTime) || null); |
| | | var data = res.data || res; |
| | | |
| | | // 1. 更新诊室名称 (从第一条数据中获取) |
| | |
| | | if (waitingArr.length > 0) currentRoomName = waitingArr[0].roomName || currentRoomName; |
| | | else if (inProgressArr.length > 0) currentRoomName = inProgressArr[0].roomName || currentRoomName; |
| | | else if (missedArr.length > 0) currentRoomName = missedArr[0].roomName || currentRoomName; |
| | | |
| | | // 兜底:接口未返回 roomName 时,从本地映射表取诊室名称 |
| | | currentRoomName = ROOM_NAME_MAP[currentRoomName] || currentRoomName; |
| | | |
| | | appState.roomName = currentRoomName; |
| | | $('currentRoomId').innerText = currentRoomName; |
| | |
| | | var patientId = currentPatient.patId || currentPatient.seqNum; |
| | | if (appState.lastSpokenPatient !== patientId) { |
| | | appState.lastSpokenPatient = patientId; |
| | | speakText(currentPatient.patName + ",请到" + appState.roomName, CALL_TIMES); |
| | | speakText(currentPatient.patName + ",请到" + appState.roomName + "就诊", CALL_TIMES); |
| | | } |
| | | } else { |
| | | appState.lastSpokenPatient = null; |
| | |
| | | } |
| | | |
| | | // ================= 渲染列表 ================= |
| | | function desensitizeName(name) { |
| | | if (!name || name.length < 2) return name || ''; |
| | | return name.charAt(0) + '*' + name.substring(2); |
| | | } |
| | | |
| | | function renderList(containerId, listData) { |
| | | var container = $(containerId); |
| | | if (!container) return; |
| | | container.innerHTML = ""; |
| | | if (listData.length === 0) { |
| | | container.innerHTML = '<div style="text-align:center; color:#ccc; width:100%; height:60px; line-height:60px; font-size:18px;">暂无患者</div>'; |
| | | container.innerHTML = '<div style="text-align:center; color:#ccc; width:100%; height:70px; line-height:70px; font-size:18px;">暂无患者</div>'; |
| | | return; |
| | | } |
| | | for (var i = 0; i < listData.length; i++) { |
| | | var item = listData[i]; |
| | | var div = document.createElement('div'); |
| | | div.className = 'patient-item'; |
| | | div.innerHTML = '<span class="p-name">' + (item.patName || '未知') + '</span>' + |
| | | '<span class="p-num">' + (item.seqNum || item.bookSeqNum || '--') + '号</span>'; |
| | | div.innerHTML = '<span class="p-num">' + (item.seqNum || '--') + '号</span> <span class="p-name">' + (item.patName ? desensitizeName(item.patName) : '未知') + '</span>'; |
| | | container.appendChild(div); |
| | | } |
| | | } |