| .vscode/launch.json | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| big.html | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| big1.html | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| big滚动.html | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| logo0.png | 补丁 | 查看 | 原始文档 | blame | 历史 | |
| small.html | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
.vscode/launch.json
@@ -8,7 +8,7 @@ "type": "editor-browser", "request": "launch", "name": "Open big.html", "url": "file:///d%3A/publish/web/call_jh/integration.html" "url": "file:///d%3A/publish/web/call_jh/big.html" } ] } big.html
@@ -1,9 +1,10 @@ <!doctype html> <!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="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, target-densitydpi=device-dpi" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <title>大å </title> <style> @@ -11,28 +12,37 @@ margin: 0; padding: 0; box-sizing: border-box; font-family: "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif; /* Android 6 å¯ç¨ä¸æåä½ */ ; font-family: "Droid Sans Fallback", "Noto Sans CJK SC", "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif; } /* 1. å ¨å±æ·±è²èæ¯ */ body { background: #001f3f; /* æ·±èè²èæ¯ï¼éåå¤§å± */ color: #fff; height: 100vh; overflow: hidden; display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-orient: vertical; -webkit-box-direction: normal; -webkit-flex-direction: column; flex-direction: column; font-size: 14px; } /* 2. 顶鍿 - ç®åè¾¹æ¡ï¼æ·±è²ä¸»é¢ */ /* 2. 顶鍿 */ .top-header { display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-align: center; -webkit-align-items: center; align-items: center; padding: 15px 30px; background: rgba(0, 30, 60, 0.9); /* æ·±è²åéæ */ white-space: nowrap; position: relative; border-bottom: 1px solid #003366; @@ -47,6 +57,7 @@ .top-header .title-text { position: absolute; left: 50%; -webkit-transform: translateX(-50%); transform: translateX(-50%); font-size: 32px; font-weight: bold; @@ -55,47 +66,125 @@ } .top-header .time-info { font-size: 24px; font-size: 20px; color: #aaa; margin-left: auto; z-index: 2; text-align: right; line-height: 1.3; } /* 3. 主ä½å 容 */ .main-content { flex: 1; .time-info-inner { display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-align: center; -webkit-align-items: center; align-items: center; } .time-left { display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-orient: vertical; -webkit-flex-direction: column; flex-direction: column; text-align: right; line-height: 1.3; } .time-right { margin-left: 8px; } .top-header .time-info .time-clock { font-size: 36px; font-weight: bold; color: #ffcc00; line-height: 1.1; } /* 3. 主ä½å 容ï¼gap æ¿æ¢ä¸º margin å ¼å®¹æ§ Chromeï¼ */ .main-content { -webkit-box-flex: 1; -webkit-flex: 1; flex: 1; display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-orient: vertical; -webkit-box-direction: normal; -webkit-flex-direction: column; flex-direction: column; padding: 15px 20px; overflow: hidden; gap: 10px; } /* åå®¹å¨ - ç§»é¤é´å½±ååè§ï¼æ´ç®æ´ */ .column-box { /* åè¡å®¹å¨ */ .columns-row { -webkit-box-flex: 1; -webkit-flex: 1; flex: 1; display: -webkit-box; display: -webkit-flex; display: flex; overflow: hidden; } .columns-row>.column-box { margin-left: 3px; margin-right: 3px; } .columns-row>.column-box:first-child { margin-left: 0; } .columns-row>.column-box:last-child { margin-right: 0; } /* åå®¹å¨ */ .column-box { display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-orient: vertical; -webkit-box-direction: normal; -webkit-flex-direction: column; flex-direction: column; background: rgba(10, 40, 80, 0.5); /* æ·±è²åéæèæ¯ */ border-radius: 0; padding: 10px; padding: 6px; -webkit-box-flex: 1; -webkit-flex: 1; flex: 1; min-width: 0; border: 1px solid #003366; } .column-box.col-wide { -webkit-box-flex: 2.5; -webkit-flex: 2.5; flex: 2.5; } .column-box.col-normal { -webkit-box-flex: 1; -webkit-flex: 1; flex: 1; } /* æ 颿 ·å¼ - ç®åä¸å线 */ .col-title { font-size: 22px; /* æ é¢è¡ */ .col-title-line { font-size: 28px; font-weight: bold; color: #4da6ff; text-align: center; } .col-title { padding-bottom: 8px; border-bottom: 1px solid #003366; margin-bottom: 10px; @@ -103,57 +192,85 @@ } .col-subtitle { font-size: 14px; color: #999; font-size: 24px; font-weight: bold; color: #ffcc00; text-align: center; min-height: 20px; /* å³ä½¿ä¸ºç©ºä¹ä¿çé«åº¦ï¼é²æ¢æ 颿 é«ä½ä¸å¹³ */ line-height: 1.4; word-break: keep-all; } /* æ£è å表 */ .patient-list { -webkit-box-flex: 1; -webkit-flex: 1; flex: 1; overflow-y: auto; padding-right: 5px; } /* ãå ³é®ä¿®æ¹ã第1æ 强å¶ä¸è¡2个 */ #col-0 { padding-right: 2px; display: -webkit-box; display: -webkit-flex; display: flex; flex-wrap: wrap; -webkit-align-content: flex-start; align-content: flex-start; } /* 第2-5æ ä¿æåå */ .col-normal .patient-list { display: flex; /* ä¸è¡ä¸¤ä¸ªï¼å¸¸è§å¿çµå¾ã卿å¿çµï¼ */ .patient-list.two-per-row { -webkit-flex-wrap: wrap; flex-wrap: wrap; } .patient-list.two-per-row .patient-item { width: 46%; margin: 0 2%; } /* ä¸è¡ä¸ä¸ª */ .patient-list.one-per-row { -webkit-box-orient: vertical; -webkit-box-direction: normal; -webkit-flex-direction: column; flex-direction: column; } /* 4. æ£è é¡¹ç® - æç®æ¨¡å¼ */ .patient-list.one-per-row .patient-item { width: 100%; } /* æ£è é¡¹ç® */ .patient-item { font-size: 22px; padding: 8px 5px; font-size: 28px; padding: 4px 3px; display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-align: center; -webkit-align-items: center; align-items: center; color: #fff; line-height: 1.4; } /* ãä¼åä¿®æ¹ã第1æ ï¼å¢å åé´è·ï¼æ¹åæ¥æ¤æ */ #col-0 .patient-item { width: 45%; /* 1. 缩å宽度ï¼ä¸ºé´éçåºç©ºé´ */ font-size: 20px; margin: 0 2.5%; /* 2. æ·»å å·¦å³å¤è¾¹è·ï¼ä¸¤åä¹é´æ»é´é为5% */ } .p-number { color: #ffcc00; font-weight: bold; margin-right: 8px; margin-right: 6px; -webkit-flex-shrink: 0; flex-shrink: 0; } .p-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: bold; } .p-name-wrap { -webkit-box-flex: 1; -webkit-flex: 1; flex: 1; overflow: hidden; text-overflow: ellipsis; @@ -163,15 +280,43 @@ .p-room { color: #4da6ff; font-weight: bold; font-size: 18px; margin-left: 5px; font-size: 22px; margin-left: 4px; -webkit-flex-shrink: 0; flex-shrink: 0; } /* 5. åºé¨æ - å± ä¸æ¾ç¤º */ /* è¿å·æ£è ï¼æå¨åå ï¼è§è§å¼±å */ .patient-item.missed { color: #aa8888; } .patient-item.missed .p-number { color: #cc9966; } .patient-item.missed .p-name { color: #aa8888; } .patient-item.missed .p-missed-tag { color: #ff6666; font-size: 22px; margin-left: 4px; -webkit-flex-shrink: 0; flex-shrink: 0; } /* åºé¨æ */ .bottom-footer { display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-pack: center; -webkit-justify-content: center; justify-content: center; /* å± ä¸ */ -webkit-box-align: center; -webkit-align-items: center; align-items: center; padding: 10px 30px; background: rgba(0, 30, 60, 0.9); @@ -185,7 +330,26 @@ text-align: center; } /* æ»å¨æ¡ç¾å - ç»ä¸ç¹ */ /* æµè¯è¯é³æé® */ #test-voice-btn { margin-left: 20px; background-color: #007bff; color: white; border: none; padding: 10px 20px; border-radius: 6px; font-size: 16px; cursor: pointer; font-weight: bold; -webkit-flex-shrink: 0; flex-shrink: 0; } #test-voice-btn:hover { background-color: #0056b3; } /* æ»å¨æ¡ */ .patient-list::-webkit-scrollbar { width: 4px; } @@ -195,16 +359,23 @@ border-radius: 2px; } /* 6. è°è¯æ¡æ ·å¼ */ /* è°è¯é¢æ¿ */ .debug-panel { display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-orient: vertical; -webkit-box-direction: normal; -webkit-flex-direction: column; flex-direction: column; position: fixed; right: 15px; bottom: 70px; width: 350px; height: 200px; background: rgba(0, 0, 0, 0.9); width: 380px; height: 220px; background: rgba(0, 0, 0, 0.92); color: #0f0; border-radius: 4px; border-radius: 6px; font-family: monospace; font-size: 12px; z-index: 9999; @@ -212,8 +383,14 @@ } .debug-header { display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-pack: justify; -webkit-justify-content: space-between; justify-content: space-between; -webkit-box-align: center; -webkit-align-items: center; align-items: center; padding: 5px; background: #333; @@ -235,6 +412,8 @@ } .debug-body { -webkit-box-flex: 1; -webkit-flex: 1; flex: 1; padding: 5px; overflow-y: auto; @@ -255,7 +434,7 @@ <!-- 顶鍿 --> <div class="top-header"> <img src="logo.png" alt="logo" /> <span class="title-text">æå¡å¤§å æå</span> <span class="title-text">å¿çµè¯åºå¤§å </span> <span class="time-info" id="headerTime"></span> </div> @@ -264,14 +443,14 @@ <!-- åºé¨æ --> <div class="bottom-footer"> <!-- 温馨æç¤ºè¯å± ä¸ --> <div class="footer-tip">温馨æç¤ºï¼è¯·å¬å°å¼å«ååå¾å¯¹åºè¯å®¤</div> <div class="footer-tip">温馨æç¤ºï¼å¬å°å¼å«å请åå¾å¯¹åºè¯å®¤ï¼è¿å·æ£è å°å¯¼è¯å°å¤çï¼</div> <button id="test-voice-btn" onclick="testVoice()">æµè¯è¯é³</button> </div> <!-- è°è¯é¢æ¿ --> <div class="debug-panel" id="debugPanel"> <div class="debug-panel" id="debugPanel" > <div class="debug-header"> <span>ð ï¸ è¿è¡æ¥å¿</span> <span>[è°è¯] è¿è¡æ¥å¿</span> <button onclick="document.getElementById('debugBody').innerHTML=''">æ¸ ç©º</button> </div> <div class="debug-body" id="debugBody"></div> @@ -279,57 +458,135 @@ <script src="./static/jquery.min.js"></script> <script> // ================= è°è¯æ¥å¿å½æ° ================= // ================= è°è¯æ¥å¿ ================= function logDebug(msg) { var now = new Date(); var timeStr = now.getHours() + ':' + now.getMinutes() + ':' + now.getSeconds(); var logHtml = '<div class="debug-line"><span class="debug-time">[' + timeStr + ']</span>' + msg + '</div>'; function pad(num) { return num < 10 ? '0' + num : '' + num; } var timeStr = pad(now.getHours()) + ':' + pad(now.getMinutes()) + ':' + pad(now.getSeconds()); var logHtml = '<div class="debug-line"><span class="debug-time">[' + timeStr + ']</span> ' + msg + '</div>'; console.log("[" + timeStr + "] " + msg); var $body = $("#debugBody"); $body.append(logHtml); $body.scrollTop($body[0].scrollHeight); var body = document.getElementById("debugBody"); if (body) { body.insertAdjacentHTML("beforeend", logHtml); // requestAnimationFrame ç¡®ä¿ DOM æ´æ°ååæ»å¨ requestAnimationFrame(function () { body.scrollTop = body.scrollHeight; }); } } // ================= åºç¨ç¶æ ================= // å¼å«å«å·æ¬¡æ°åæ°ï¼å¼å«æ£è å°±è¯æ¶é夿æ¥ç次æ°ï¼é»è®¤ 2 次 var CALL_TIMES = 2; // ä¸è¡ä¸¤ä¸ªæ£è çåç´¢å¼ï¼å¸¸è§å¿çµå¾ã卿å¿çµï¼ var TWO_PER_ROW_COLS = [0, 1]; // é£éçµççæ¯å¦æ¾ç¤ºï¼false éèï¼true æ¾ç¤ºï¼ var SHOW_COL_ESOPHAGEAL = false; var appState = { columnTitles: ["常è§å¿çµå¾", "卿å¿çµ", "å¹³æ¿è¿å¨å¿çµ", "é£éçµçç", "å¨è硬åçæµ"], columnSubTitles: ["åºè¾¹å¿çµå¾(常è§+é¢è°±)M / å¿çµåéå¾N", "卿è¡åC", "", "", ""], columnTitles: ["常è§å¿çµå¾/å¿çµåéå¾", "卿å¿çµ/è¡å", "å¹³æ¿è¿å¨å¿çµ", "é£éçµçç", "å¨è硬åçæµ"], columnSubTitles: ["1å·-2å·è¯å®¤", "2å·è¯å®¤", "4å·è¯å®¤", "6å·è¯å®¤", "5å·è¯å®¤"], patients: [], apiBaseUrl: "http://192.168.3.12/admin-api", // apiBaseUrl: "http://192.168.3.12/admin-api", apiBaseUrl: "http://10.0.2.193/admin-api", pollTimer: null, callRepeatTimes: 2, spokenPatients: {}, ttsQueue: [], isSpeaking: false isSpeaking: false, }; var patStatus = { 3: "å·²è¿å·-æé", 5: "å·²è¿å·", 7: "å·²è¿å·-å®è£ ", 10: "æéä¸", 12: "亲å", 13: "亲å-å®è£ ", 15: "å·²å¬å", 20: "åè¯ä¸", 30: "å°±è¯ä¸", 33: "å·²é¢ç¨", 34: "å·²å¬å-å®è£ ", 36: "å®è£ ä¸", 40: "已就è¯" }; // ================= è¯é³ææ¥ ================= // Android WebView GC ä¿æ¤ï¼å ¨å±ææ utterance å¼ç¨ï¼é²æ¢è¢«åæ¶å¯¼è´æ 声 var _gCurrentUtterance = null; function processTtsQueue() { if (appState.isSpeaking || appState.ttsQueue.length === 0) return; appState.isSpeaking = true; var text = appState.ttsQueue.shift(); logDebug("ð å¼å§ææ¥: " + text); logDebug("[ææ¥] " + text); var utterance = new SpeechSynthesisUtterance(text); utterance.onend = function () { appState.isSpeaking = false; processTtsQueue(); }; utterance.onerror = function () { appState.isSpeaking = false; processTtsQueue(); }; // ä¼å å°è¯è®¾å¤åç TTS æ¥å£ (wowjoy) if (typeof wowjoy !== 'undefined' && typeof wowjoy.speek === 'function') { try { wowjoy.speek(text); logDebug("[ææ¥] wowjoy.speek å·²è°ç¨"); setTimeout(function () { appState.isSpeaking = false; processTtsQueue(); }, 4000); return; } catch (e) { logDebug("â wowjoy è°ç¨å¤±è´¥: " + e.message); } } catch (e) { logDebug("[wowjoy失败] " + e.message); } } if (window.speechSynthesis) { window.speechSynthesis.speak(utterance); } else { appState.isSpeaking = false; processTtsQueue(); } if (!window.speechSynthesis) { logDebug("[TTS] æµè§å¨ä¸æ¯æè¯é³åæ"); appState.isSpeaking = false; processTtsQueue(); return; } // ä» å¨æè¯é³æ£å¨ææ¾æ¶æ cancelï¼é¿å ä¸å¿ è¦ç䏿 if (window.speechSynthesis.speaking) { logDebug("[ææ¥] cancel å½åææ¾"); window.speechSynthesis.cancel(); } var utterance = new SpeechSynthesisUtterance(text); utterance.rate = 0.85; utterance.volume = 1.0; // Android WebView GC ä¿æ¤ï¼åå ¨å±å¼ç¨ _gCurrentUtterance = utterance; utterance.onstart = function () { logDebug("[ææ¥] onstart 触å"); }; utterance.onend = function () { logDebug("[ææ¥] onend 触å"); _gCurrentUtterance = null; appState.isSpeaking = false; // å»¶è¿ä¸ä¸è®©å¼æå®å ¨éæ¾ setTimeout(function () { processTtsQueue(); }, 200); }; utterance.onerror = function (e) { logDebug("[TTSé误] " + (e.error || "unknown")); _gCurrentUtterance = null; appState.isSpeaking = false; setTimeout(function () { processTtsQueue(); }, 200); }; // ç¡®ä¿è¯é³å å·²å è½½ (Android ä¸ getVoices å¯è½åå§ä¸ºç©º) var voices = window.speechSynthesis.getVoices(); if (voices.length > 0) { utterance.voice = voices[0]; logDebug("[ææ¥] 使ç¨è¯é³: " + voices[0].name + " lang=" + voices[0].lang); } else { logDebug("[ææ¥] getVoices 为空, çå¾ voiceschanged äºä»¶"); var voicesLoaded = false; var onVoicesChange = function () { if (voicesLoaded) return; voicesLoaded = true; var v2 = window.speechSynthesis.getVoices(); if (v2.length > 0) { utterance.voice = v2[0]; logDebug("[ææ¥] è¯é³å è½½å®æ: " + v2[0].name); } window.speechSynthesis.speak(utterance); }; window.speechSynthesis.addEventListener('voiceschanged', onVoicesChange); // 妿 1s å æ²¡è§¦åï¼ç´æ¥ speak setTimeout(function () { if (!voicesLoaded) { logDebug("[ææ¥] voiceschanged è¶ æ¶, ç´æ¥ speak"); window.speechSynthesis.speak(utterance); } }, 1000); return; } window.speechSynthesis.speak(utterance); logDebug("[ææ¥] speak å·²è°ç¨"); } function speak(text) { @@ -338,46 +595,179 @@ processTtsQueue(); } // ================= 渲æå½æ° ================= function renderMainContent() { var $main = $("#mainContent"); $main.empty(); // ================= è¯é³æµè¯ ================= function testVoice() { logDebug("[è¯é³æµè¯] ========== å¼å§ =========="); var testText = "æµè¯è¯é³ææ¥"; for (var i = 0; i < appState.columnTitles.length; i++) { var titleHtml = '<div class="col-title-line">' + appState.columnTitles[i] + '</div>'; if (appState.columnSubTitles[i]) { titleHtml += '<div class="col-subtitle">' + appState.columnSubTitles[i] + '</div>'; // 1. æ£æµ wowjoy åçæ¥å£ var hasWowjoy = (typeof wowjoy !== 'undefined'); logDebug("[è¯é³æµè¯] wowjoy 对象åå¨=" + hasWowjoy); if (hasWowjoy) { logDebug("[è¯é³æµè¯] wowjoy.speek ç±»å=" + (typeof wowjoy.speek)); if (typeof wowjoy.speek === 'function') { try { wowjoy.speek(testText); logDebug("[è¯é³æµè¯] wowjoy.speek å·²è°ç¨ï¼æ³¨æå¬è®¾å¤å£°é³"); } catch (e) { logDebug("[è¯é³æµè¯] wowjoy.speek å¼å¸¸: " + e.message); } } } // 2. æ£æµ SpeechSynthesis var hasSpeech = (typeof window.speechSynthesis !== 'undefined'); logDebug("[è¯é³æµè¯] speechSynthesis 对象åå¨=" + hasSpeech); if (hasSpeech) { var syn = window.speechSynthesis; logDebug("[è¯é³æµè¯] speaking=" + syn.speaking + " pending=" + syn.pending + " paused=" + syn.paused); var voices = syn.getVoices(); logDebug("[è¯é³æµè¯] getVoices è¿å " + voices.length + " 个è¯é³å "); for (var v = 0; v < Math.min(voices.length, 5); v++) { logDebug("[è¯é³æµè¯] è¯é³[" + v + "] " + voices[v].name + " lang=" + voices[v].lang + " local=" + voices[v].localService); } var colClass = (i === 0) ? 'col-wide' : 'col-normal'; // 注æï¼è¿éä¸åç»ç¬¬1æ å col-flexï¼ç± CSS #col-0 å¼ºå¶æ§å¶ var colHtml = '<div class="column-box ' + colClass + '">' + '<div class="col-title">' + titleHtml + '</div>' + '<div class="patient-list" id="col-' + i + '"></div>' + '</div>'; $main.append(colHtml); // 妿 voices 为空ï¼çå¾ å è½½ååæ function doSpeakTest() { var v2 = syn.getVoices(); var u = new SpeechSynthesisUtterance(testText); u.rate = 0.85; u.volume = 1.0; if (v2.length > 0) { u.voice = v2[0]; logDebug("[è¯é³æµè¯] éæ©è¯é³: " + v2[0].name); } // GC ä¿æ¤ window._testUtterance = u; u.onstart = function() { logDebug("[è¯é³æµè¯] onstart 触å â"); }; u.onend = function() { logDebug("[è¯é³æµè¯] onend 触å â"); window._testUtterance = null; }; u.onerror = function(e) { logDebug("[è¯é³æµè¯] onerror: " + (e.error || "unknown")); window._testUtterance = null; }; if (syn.speaking) { logDebug("[è¯é³æµè¯] æè¯é³æ£å¨ææ¾ï¼å cancel"); syn.cancel(); setTimeout(function () { syn.speak(u); logDebug("[è¯é³æµè¯] speak å·²è°ç¨(å»¶è¿)"); }, 300); } else { syn.speak(u); logDebug("[è¯é³æµè¯] speak å·²è°ç¨"); } } if (voices.length === 0) { logDebug("[è¯é³æµè¯] è¯é³å表为空ï¼çå¾ voiceschanged..."); var loaded = false; var handler = function () { if (loaded) return; loaded = true; logDebug("[è¯é³æµè¯] voiceschanged 触å, è¯é³æ°=" + syn.getVoices().length); doSpeakTest(); }; syn.addEventListener('voiceschanged', handler); setTimeout(function () { if (!loaded) { logDebug("[è¯é³æµè¯] voiceschanged è¶ æ¶(2s), å¼ºå¶æµè¯"); doSpeakTest(); } }, 2000); } else { doSpeakTest(); } } if (!hasWowjoy && !hasSpeech) { logDebug("[è¯é³æµè¯] æ ä»»ä½ TTS 弿å¯ç¨ï¼"); } logDebug("[è¯é³æµè¯] ========== ç»æ =========="); } function renderPatients() { for (var i = 0; i < appState.columnTitles.length; i++) { $("#col-" + i).empty(); } // ================= 渲æï¼æ 颿¡æ¶ï¼ä» åå§å䏿¬¡ï¼================= function initLayout() { logDebug("[å¸å±] initLayout å¼å§, åæ°=" + appState.columnTitles.length); var main = document.getElementById("mainContent"); if (!main) { logDebug("[å¸å±] mainContent å ç´ æªæ¾å°!"); return; } var html = '<div class="columns-row">'; var visibleCount = 0; for (var c = 0; c < appState.patients.length; c++) { var colData = appState.patients[c]; if (Array.isArray(colData)) { var $col = $("#col-" + c); for (var p = 0; p < colData.length; p++) { var pat = colData[p]; var roomHtml = pat.roomName ? '<span class="p-room">(' + pat.roomName + ')</span>' : ''; var itemHtml = '<div class="patient-item">' + '<span class="p-number">' + (pat.bookSeqNum || '') + '</span>' + '<span class="p-name">' + (pat.patName || '') + '</span>' + roomHtml + '</div>'; $col.append(itemHtml); } for (var i = 0; i < appState.columnTitles.length; i++) { // é£éçµççï¼éè¿ SHOW_COL_ESOPHAGEAL æ§å¶æ¾é if (i === 3 && !SHOW_COL_ESOPHAGEAL) { logDebug("[å¸å±] å" + i + " é£éçµçç å·²éè"); continue; } var subtitle = appState.columnSubTitles[i] || " "; // ä¸è¡ä¸¤ä¸ªçåç¨ col-wideï¼å¦å col-normal var isTwoPerRow = TWO_PER_ROW_COLS.indexOf(i) !== -1; var colClass = isTwoPerRow ? 'col-wide' : 'col-normal'; var rowClass = isTwoPerRow ? 'two-per-row' : 'one-per-row'; logDebug("[å¸å±] å" + i + " " + appState.columnTitles[i] + " class=" + colClass + " row=" + rowClass); html += '<div class="column-box ' + colClass + '">' + '<div class="col-title">' + '<div class="col-title-line">' + appState.columnTitles[i] + '</div>' + '<div class="col-subtitle">' + subtitle + '</div>' + '</div>' + '<div class="patient-list ' + rowClass + '" id="col-' + i + '"></div>' + '</div>'; visibleCount++; } html += '</div>'; main.innerHTML = html; logDebug("[å¸å±] initLayout 宿, å¯è§å=" + visibleCount); } // ================= 渲æï¼æ£è å表ï¼diff æ´æ°ï¼åªæ¹ååçåï¼================= function desensitizeName(name) { if (!name || name.length < 2) return name || ''; return name.charAt(0) + '*' + name.substring(2); } function isMissedStatus(status) { var s = parseInt(status, 10); return s === 3 || s === 5 || s === 7; } function buildColumnHtml(colData) { if (!Array.isArray(colData) || colData.length === 0) return ""; var h = ""; for (var p = 0; p < colData.length; p++) { var pat = colData[p]; var missedClass = isMissedStatus(pat.status) ? ' missed' : ''; var roomHtml = (!isMissedStatus(pat.status) && pat.roomName) ? '<span class="p-room">' + pat.roomName + '</span>' : ''; h += '<div class="patient-item' + missedClass + '">' + '<span class="p-number">' + (pat.seqNum || '') + '</span>' + '<span class="p-name-wrap"><span class="p-name">' + desensitizeName(pat.patName) + '</span>' + roomHtml + (missedClass ? '<span class="p-missed-tag">è¿å·</span>' : '') + '</span>' + '</div>'; } return h; } function isSameColumn(a, b) { if (!Array.isArray(a) || !Array.isArray(b)) return a === b; if (a.length !== b.length) return false; for (var i = 0; i < a.length; i++) { if (a[i].patId !== b[i].patId) return false; if (a[i].roomName !== b[i].roomName) return false; if (parseInt(a[i].status, 10) !== parseInt(b[i].status, 10)) return false; } return true; } function updatePatients(dataList) { for (var i = 0; i < appState.columnTitles.length; i++) { var newData = (dataList && dataList[i]) ? dataList[i] : []; var oldData = appState.patients[i]; // diffï¼æ°æ®æªåååè·³è¿è¯¥å DOM æä½ if (isSameColumn(oldData, newData)) continue; var col = document.getElementById("col-" + i); if (col) { col.innerHTML = buildColumnHtml(newData); logDebug("[æ´æ°] å" + i + " " + appState.columnTitles[i] + " å·æ° " + newData.length + " 人"); } else { logDebug("[æ´æ°] å" + i + " DOMä¸åå¨(å¯è½å·²éè)"); } } } @@ -404,12 +794,12 @@ } if (!oldPat || !oldPat.roomName) { var repeatText = ""; for (var r = 0; r < appState.callRepeatTimes; r++) { repeatText += "请 " + newPat.bookSeqNum + " å· " + newPat.patName + " å° " + newPat.roomName + " å°±è¯ã"; } speak(repeatText); repeatText += "请 " + newPat.seqNum + " å· " + newPat.patName + " å° " + newPat.roomName + " å°±è¯ã"; for (var r = 0; r < CALL_TIMES; r++) { speak(repeatText); } appState.spokenPatients[newPat.patId] = true; logDebug("ð å å ¥ææ¥éå: " + newPat.patName); logDebug("[æé] " + newPat.patName + " -> " + newPat.roomName); } } } @@ -420,51 +810,84 @@ // ================= æ°æ®è·å ================= function fetchQueueData() { var url = appState.apiBaseUrl + "/ecg/screen/big-screen-data"; logDebug("[请æ±] GET " + url); $.ajax({ url: url, type: "GET", dataType: "json", timeout: 5000, success: function (res) { logDebug("[ååº] code=" + (res ? res.code : "null")); var dataList = []; if (res && res.code === 0 && res.data) { for (var i = 0; i < appState.columnTitles.length; i++) { var key = i.toString(); dataList[i] = (res.data[key] && Array.isArray(res.data[key])) ? res.data[key] : []; logDebug("[æ°æ®] å" + i + " " + appState.columnTitles[i] + " æ£è æ°=" + dataList[i].length); } } else { logDebug("[ååº] å¼å¸¸ code=" + (res ? res.code : "null") + " msg=" + (res ? res.msg : "")); } // å diff æ´æ° DOMï¼åææ¥ï¼æåä¿åæ°æ®ç¨äºä¸æ¬¡å¯¹æ¯ updatePatients(dataList); checkAndSpeakNewRooms(appState.patients, dataList); appState.patients = dataList; renderPatients(); if (window.performance && window.performance.memory) { var usedMB = (window.performance.memory.usedJSHeapSize / 1048576).toFixed(2); logDebug("ð¾ å å: " + usedMB + " MB"); } // Android 6 WebView é叏䏿¯æ performance.memoryï¼å®å ¨å®å« try { if (window.performance && window.performance.memory) { var usedMB = (window.performance.memory.usedJSHeapSize / 1048576).toFixed(2); logDebug("[å å] " + usedMB + " MB"); } } catch (e) { } }, error: function (err) { logDebug("â 请æ±å¤±è´¥: " + err.statusText); logDebug("[请æ±å¤±è´¥] status=" + err.status + " " + (err.statusText || "ç½ç»é误") + " url=" + url); } }); } // ================= åå§å ================= $(document).ready(function () { renderMainContent(); function onReady() { logDebug("[åå§å] onReady å¼å§"); logDebug("[ç¯å¢] speechSynthesis=" + (typeof window.speechSynthesis !== 'undefined') + " wowjoy=" + (typeof wowjoy !== 'undefined')); try { initLayout(); logDebug("[åå§å] initLayout 宿"); } catch (e) { logDebug("[åå§å] initLayout å¼å¸¸: " + e.message); } updateHeaderTime(); setInterval(updateHeaderTime, 1000); fetchQueueData(); try { fetchQueueData(); logDebug("[åå§å] 馿¬¡ fetchQueueData å·²åèµ·"); } catch (e) { logDebug("[åå§å] fetchQueueData å¼å¸¸: " + e.message); } appState.pollTimer = setInterval(fetchQueueData, 5000); logDebug("â ç³»ç»å¯å¨"); }); logDebug("[ç³»ç»] å¯å¨å®æ - Android 6.0.1 轮询é´é5s"); } function updateHeaderTime() { var now = new Date(); var weekDays = ["æææ¥", "ææä¸", "ææäº", "ææä¸", "ææå", "ææäº", "ææå "]; function padZero(num) { return num < 10 ? '0' + num : '' + num; } var dateStr = now.getFullYear() + "å¹´" + padZero(now.getMonth() + 1) + "æ" + padZero(now.getDate()) + "æ¥ " + weekDays[now.getDay()]; var dateStr = now.getFullYear() + "å¹´" + padZero(now.getMonth() + 1) + "æ" + padZero(now.getDate()) + "æ¥"; var weekStr = weekDays[now.getDay()]; var timeStr = padZero(now.getHours()) + ":" + padZero(now.getMinutes()); $("#headerTime").text(dateStr + " " + timeStr); var el = document.getElementById("headerTime"); // ææåæ¥æåå ä¸è¡å¨å·¦ï¼æ¶é´å¤§åå¨å³è·¨ä¸¤è¡ if (el) el.innerHTML = '<div class="time-info-inner"><div class="time-left"><div>' + weekStr + '</div><div>' + dateStr + '</div></div><div class="time-right"><span class="time-clock">' + timeStr + '</span></div></div>'; } // å ¼å®¹ DOM readyï¼Android 6 æäº WebView å¯è½æ²¡æ $ï¼ logDebug("[å ¥å£] readyState=" + document.readyState + " jQuery=" + (typeof $ !== 'undefined')); if (typeof $ !== 'undefined') { logDebug("[å ¥å£] ä½¿ç¨ jQuery.ready"); $(document).ready(onReady); } else if (document.readyState === 'complete' || document.readyState === 'interactive') { logDebug("[å ¥å£] DOM已就绪, setTimeout触å"); setTimeout(onReady, 1); } else { logDebug("[å ¥å£] çå¬ DOMContentLoaded"); document.addEventListener('DOMContentLoaded', onReady); } </script> </body> big1.html
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,680 @@ <!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, target-densitydpi=device-dpi" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <title>大å </title> <style> * { margin: 0; padding: 0; box-sizing: border-box; /* Android 6 å¯ç¨ä¸æåä½ */ ; font-family: "Droid Sans Fallback", "Noto Sans CJK SC", "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif; } /* 1. å ¨å±æ·±è²èæ¯ */ body { background: #001f3f; color: #fff; height: 100vh; overflow: hidden; display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-orient: vertical; -webkit-box-direction: normal; -webkit-flex-direction: column; flex-direction: column; font-size: 14px; } /* 2. 顶鍿 */ .top-header { display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-align: center; -webkit-align-items: center; align-items: center; padding: 15px 30px; background: rgba(0, 30, 60, 0.9); white-space: nowrap; position: relative; border-bottom: 1px solid #003366; } .top-header img { height: 50px; margin-right: 20px; z-index: 2; } .top-header .title-text { position: absolute; left: 50%; -webkit-transform: translateX(-50%); transform: translateX(-50%); font-size: 32px; font-weight: bold; color: #fff; z-index: 1; } .top-header .time-info { font-size: 20px; color: #aaa; margin-left: auto; z-index: 2; text-align: right; line-height: 1.3; } .time-info-inner { display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-align: center; -webkit-align-items: center; align-items: center; } .time-left { display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-orient: vertical; -webkit-flex-direction: column; flex-direction: column; text-align: right; line-height: 1.3; } .time-right { margin-left: 8px; } .top-header .time-info .time-clock { font-size: 36px; font-weight: bold; color: #ffcc00; line-height: 1.1; } /* 3. 主ä½å 容ï¼gap æ¿æ¢ä¸º margin å ¼å®¹æ§ Chromeï¼ */ .main-content { -webkit-box-flex: 1; -webkit-flex: 1; flex: 1; display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-orient: vertical; -webkit-box-direction: normal; -webkit-flex-direction: column; flex-direction: column; padding: 15px 20px; overflow: hidden; } /* åè¡å®¹å¨ */ .columns-row { -webkit-box-flex: 1; -webkit-flex: 1; flex: 1; display: -webkit-box; display: -webkit-flex; display: flex; overflow: hidden; } .columns-row>.column-box { margin-left: 5px; margin-right: 5px; } .columns-row>.column-box:first-child { margin-left: 0; } .columns-row>.column-box:last-child { margin-right: 0; } /* åå®¹å¨ */ .column-box { display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-orient: vertical; -webkit-box-direction: normal; -webkit-flex-direction: column; flex-direction: column; background: rgba(10, 40, 80, 0.5); border-radius: 0; padding: 10px; -webkit-box-flex: 1; -webkit-flex: 1; flex: 1; min-width: 0; border: 1px solid #003366; } .column-box.col-wide { -webkit-box-flex: 2.5; -webkit-flex: 2.5; flex: 2.5; } .column-box.col-normal { -webkit-box-flex: 1; -webkit-flex: 1; flex: 1; } /* æ é¢è¡ */ .col-title-line { font-size: 22px; font-weight: bold; color: #4da6ff; text-align: center; } .col-title { padding-bottom: 8px; border-bottom: 1px solid #003366; margin-bottom: 10px; text-align: center; } .col-subtitle { font-size: 14px; color: #999; text-align: center; min-height: 20px; /* å³ä½¿ä¸ºç©ºä¹ä¿çé«åº¦ï¼é²æ¢æ 颿 é«ä½ä¸å¹³ */ ; line-height: 1.4; word-break: keep-all; } /* æ£è å表 */ .patient-list { -webkit-box-flex: 1; -webkit-flex: 1; flex: 1; overflow-y: auto; padding-right: 5px; } /* 第1æ ï¼ä¸è¡ä¸¤ä¸ª */ #col-0 { display: -webkit-box; display: -webkit-flex; display: flex; -webkit-flex-wrap: wrap; flex-wrap: wrap; -webkit-align-content: flex-start; align-content: flex-start; } .col-normal .patient-list { display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-orient: vertical; -webkit-box-direction: normal; -webkit-flex-direction: column; flex-direction: column; } /* æ£è é¡¹ç® */ .patient-item { font-size: 22px; padding: 8px 5px; display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-align: center; -webkit-align-items: center; align-items: center; color: #fff; line-height: 1.4; } #col-0 .patient-item { width: 45%; font-size: 20px; margin: 0 2.5%; } .p-number { color: #ffcc00; font-weight: bold; margin-right: 8px; } .p-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .p-name-wrap { -webkit-box-flex: 1; -webkit-flex: 1; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .p-room { color: #4da6ff; font-weight: bold; font-size: 18px; margin-left: 4px; } /* è¿å·æ£è ï¼æå¨åå ï¼è§è§å¼±å */ .patient-item.missed { color: #aa8888; } .patient-item.missed .p-number { color: #cc9966; } .patient-item.missed .p-missed-tag { color: #ff6666; font-size: 16px; margin-left: 4px; -webkit-flex-shrink: 0; flex-shrink: 0; } /* åºé¨æ */ .bottom-footer { display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-pack: center; -webkit-justify-content: center; justify-content: center; -webkit-box-align: center; -webkit-align-items: center; align-items: center; padding: 10px 30px; background: rgba(0, 30, 60, 0.9); border-top: 1px solid #003366; } .footer-tip { font-size: 22px; color: #ffcc00; font-weight: bold; text-align: center; } /* æ»å¨æ¡ */ .patient-list::-webkit-scrollbar { width: 4px; } .patient-list::-webkit-scrollbar-thumb { background: #444; border-radius: 2px; } /* è°è¯é¢æ¿ */ .debug-panel { display: none; /* éè¦è°è¯æ¶å ææ¤è¡å³å¯æ¾ç¤º */ position: fixed; right: 15px; bottom: 70px; width: 350px; height: 200px; background: rgba(0, 0, 0, 0.9); color: #0f0; border-radius: 4px; font-family: monospace; font-size: 12px; z-index: 9999; overflow: visible; } .debug-header { display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-pack: justify; -webkit-justify-content: space-between; justify-content: space-between; -webkit-box-align: center; -webkit-align-items: center; align-items: center; padding: 5px; background: #333; border-bottom: 1px solid #000; } .debug-header span { font-weight: bold; font-size: 13px; } .debug-header button { background: #c00; color: #fff; border: none; padding: 2px 6px; font-size: 10px; cursor: pointer; } .debug-body { -webkit-box-flex: 1; -webkit-flex: 1; flex: 1; padding: 5px; overflow-y: auto; font-size: 11px; } .debug-line { margin-bottom: 2px; } .debug-time { color: #888; } </style> </head> <body> <!-- 顶鍿 --> <div class="top-header"> <img src="logo.png" alt="logo" /> <span class="title-text">å¿çµæ£æ¥æå¡è¯åºå¤§å </span> <span class="time-info" id="headerTime"></span> </div> <!-- 主ä½å 容 --> <div class="main-content" id="mainContent"></div> <!-- åºé¨æ --> <div class="bottom-footer"> <div class="footer-tip">温馨æç¤ºï¼å¬å°å¼å«å请åå¾å¯¹åºè¯å®¤ï¼è¿å·æ£è å°å¯¼è¯å°å¤çï¼</div> </div> <!-- è°è¯é¢æ¿ --> <div class="debug-panel" id="debugPanel" > <div class="debug-header"> <span>[è°è¯] è¿è¡æ¥å¿</span> <button onclick="document.getElementById('debugBody').innerHTML=''">æ¸ ç©º</button> </div> <div class="debug-body" id="debugBody"></div> </div> <script src="./static/jquery.min.js"></script> <script> // ================= è°è¯æ¥å¿ ================= function logDebug(msg) { var now = new Date(); function pad(num) { return num < 10 ? '0' + num : '' + num; } var timeStr = pad(now.getHours()) + ':' + pad(now.getMinutes()) + ':' + pad(now.getSeconds()); var logHtml = '<div class="debug-line"><span class="debug-time">[' + timeStr + ']</span> ' + msg + '</div>'; console.log("[" + timeStr + "] " + msg); var body = document.getElementById("debugBody"); if (body) { body.insertAdjacentHTML("beforeend", logHtml); body.scrollTop = body.scrollHeight; } } // ================= åºç¨ç¶æ ================= // å¼å«å«å·æ¬¡æ°åæ°ï¼å¼å«æ£è å°±è¯æ¶é夿æ¥ç次æ°ï¼é»è®¤ 2 次 var CALL_REPEAT_TIMES = 2; var appState = { columnTitles: ["常è§å¿çµå¾", "卿å¿çµå¾/è¡å", "å¿çµå¾è¿å¨è¯éª", "é£éçµçç", "å¨è硬åçæµ"], columnSubTitles: ["åºè¾¹å¿çµå¾(常è§+é¢è°±) / å¿çµåéå¾", "", "", "", ""], patients: [], apiBaseUrl: "http://localhost/admin-api", // apiBaseUrl: "http://10.0.2.193/admin-api", pollTimer: null, spokenPatients: {}, ttsQueue: [], isSpeaking: false, }; var patStatus = { 3: "å·²è¿å·-æé", 5: "å·²è¿å·", 7: "å·²è¿å·-å®è£ ", 10: "æéä¸", 12: "亲å", 13: "亲å-å®è£ ", 15: "å·²å¬å", 20: "åè¯ä¸", 30: "å°±è¯ä¸", 33: "å·²é¢ç¨", 34: "å·²å¬å-å®è£ ", 36: "å®è£ ä¸", 40: "已就è¯" }; // ================= è¯é³ææ¥ ================= function processTtsQueue() { if (appState.isSpeaking || appState.ttsQueue.length === 0) return; appState.isSpeaking = true; var text = appState.ttsQueue.shift(); logDebug("[ææ¥] " + text); var utterance = new SpeechSynthesisUtterance(text); // Android 6 é»è®¤è¯éå¯è½å¾å¿«ï¼éå½è°æ ¢ utterance.rate = 0.85; utterance.onend = function () { appState.isSpeaking = false; processTtsQueue(); }; utterance.onerror = function (e) { logDebug("[TTSé误] " + (e.error || "unknown")); appState.isSpeaking = false; processTtsQueue(); }; // ä¼å å°è¯è®¾å¤åç TTS æ¥å£ if (typeof wowjoy !== 'undefined' && typeof wowjoy.speek === 'function') { try { wowjoy.speek(text); setTimeout(function () { appState.isSpeaking = false; processTtsQueue(); }, 4000); return; } catch (e) { logDebug("[wowjoy失败] " + e.message); } } if (window.speechSynthesis) { // Android 6 WebView ææ¶éè¦å cancel å speak window.speechSynthesis.cancel(); window.speechSynthesis.speak(utterance); } else { logDebug("[TTS] æµè§å¨ä¸æ¯æè¯é³åæ"); appState.isSpeaking = false; processTtsQueue(); } } function speak(text) { if (!text) return; appState.ttsQueue.push(text); processTtsQueue(); } // ================= 渲æï¼æ 颿¡æ¶ï¼ä» åå§å䏿¬¡ï¼================= function initLayout() { var main = document.getElementById("mainContent"); var html = '<div class="columns-row">'; for (var i = 0; i < appState.columnTitles.length; i++) { // æ é¢å坿 é¢é½æåºå®å ä½ï¼é¿å æ/æ 坿 颿¶é«åº¦è·³å¨ var subtitle = appState.columnSubTitles[i] || " "; var colClass = (i === 0) ? 'col-wide' : 'col-normal'; html += '<div class="column-box ' + colClass + '">' + '<div class="col-title">' + '<div class="col-title-line">' + appState.columnTitles[i] + '</div>' + '<div class="col-subtitle">' + subtitle + '</div>' + '</div>' + '<div class="patient-list" id="col-' + i + '"></div>' + '</div>'; } html += '</div>'; main.innerHTML = html; } // ================= 渲æï¼æ£è å表ï¼diff æ´æ°ï¼åªæ¹ååçåï¼================= function desensitizeName(name) { if (!name || name.length < 2) return name || ''; return name.charAt(0) + '*' + name.substring(2); } function isMissedStatus(status) { var s = parseInt(status, 10); return s === 3 || s === 5 || s === 7; } function buildColumnHtml(colData) { if (!Array.isArray(colData) || colData.length === 0) return ""; var h = ""; for (var p = 0; p < colData.length; p++) { var pat = colData[p]; var missedClass = isMissedStatus(pat.status) ? ' missed' : ''; var roomHtml = (!isMissedStatus(pat.status) && pat.roomName) ? '<span class="p-room">(' + pat.roomName + ')</span>' : ''; h += '<div class="patient-item' + missedClass + '">' + '<span class="p-number">' + (pat.seqNum || '') + '</span>' + '<span class="p-name-wrap"><span class="p-name">' + desensitizeName(pat.patName) + '</span>' + roomHtml + (missedClass ? '<span class="p-missed-tag">ï¼è¿å·ï¼</span>' : '') + '</span>' + '</div>'; } return h; } function isSameColumn(a, b) { if (!Array.isArray(a) || !Array.isArray(b)) return a === b; if (a.length !== b.length) return false; for (var i = 0; i < a.length; i++) { if (a[i].patId !== b[i].patId) return false; if (a[i].roomName !== b[i].roomName) return false; if (parseInt(a[i].status, 10) !== parseInt(b[i].status, 10)) return false; } return true; } function updatePatients(dataList) { for (var i = 0; i < appState.columnTitles.length; i++) { var newData = (dataList && dataList[i]) ? dataList[i] : []; var oldData = appState.patients[i]; // diffï¼æ°æ®æªåååè·³è¿è¯¥å DOM æä½ if (isSameColumn(oldData, newData)) continue; var col = document.getElementById("col-" + i); if (col) col.innerHTML = buildColumnHtml(newData); } } // ================= æ°æ®å¯¹æ¯ä¸ææ¥ ================= function checkAndSpeakNewRooms(oldData, newData) { if (!oldData || oldData.length === 0) return; for (var c = 0; c < newData.length; c++) { var newCol = newData[c]; var oldCol = oldData[c]; if (Array.isArray(newCol)) { for (var p = 0; p < newCol.length; p++) { var newPat = newCol[p]; if (newPat.roomName) { if (appState.spokenPatients[newPat.patId]) continue; var oldPat = null; if (Array.isArray(oldCol)) { for (var k = 0; k < oldCol.length; k++) { if (oldCol[k].patId === newPat.patId) { oldPat = oldCol[k]; break; } } } if (!oldPat || !oldPat.roomName) { var repeatText = ""; repeatText += "请 " + newPat.seqNum + " å· " + newPat.patName + " å° " + newPat.roomName + " å°±è¯ã"; for (var r = 0; r < CALL_REPEAT_TIMES; r++) { speak(repeatText); } appState.spokenPatients[newPat.patId] = true; logDebug("[æé] " + newPat.patName + " -> " + newPat.roomName); } } } } } } // ================= æ°æ®è·å ================= function fetchQueueData() { var url = appState.apiBaseUrl + "/ecg/screen/big-screen-data"; $.ajax({ url: url, type: "GET", dataType: "json", timeout: 5000, success: function (res) { var dataList = []; if (res && res.code === 0 && res.data) { for (var i = 0; i < appState.columnTitles.length; i++) { var key = i.toString(); dataList[i] = (res.data[key] && Array.isArray(res.data[key])) ? res.data[key] : []; } } // å diff æ´æ° DOMï¼åææ¥ï¼æåä¿åæ°æ®ç¨äºä¸æ¬¡å¯¹æ¯ updatePatients(dataList); checkAndSpeakNewRooms(appState.patients, dataList); appState.patients = dataList; // Android 6 WebView é叏䏿¯æ performance.memoryï¼å®å ¨å®å« try { if (window.performance && window.performance.memory) { var usedMB = (window.performance.memory.usedJSHeapSize / 1048576).toFixed(2); logDebug("[å å] " + usedMB + " MB"); } } catch (e) { } }, error: function (err) { logDebug("[请æ±å¤±è´¥] " + (err.statusText || "ç½ç»é误")); } }); } // ================= åå§å ================= function onReady() { initLayout(); updateHeaderTime(); setInterval(updateHeaderTime, 1000); fetchQueueData(); appState.pollTimer = setInterval(fetchQueueData, 5000); logDebug("[ç³»ç»] å¯å¨å®æ - Android 6.0.1"); } function updateHeaderTime() { var now = new Date(); var weekDays = ["æææ¥", "ææä¸", "ææäº", "ææä¸", "ææå", "ææäº", "ææå "]; function padZero(num) { return num < 10 ? '0' + num : '' + num; } var dateStr = now.getFullYear() + "å¹´" + padZero(now.getMonth() + 1) + "æ" + padZero(now.getDate()) + "æ¥"; var weekStr = weekDays[now.getDay()]; var timeStr = padZero(now.getHours()) + ":" + padZero(now.getMinutes()); var el = document.getElementById("headerTime"); // ææåæ¥æåå ä¸è¡å¨å·¦ï¼æ¶é´å¤§åå¨å³è·¨ä¸¤è¡ if (el) el.innerHTML = '<div class="time-info-inner"><div class="time-left"><div>' + weekStr + '</div><div>' + dateStr + '</div></div><div class="time-right"><span class="time-clock">' + timeStr + '</span></div></div>'; } // å ¼å®¹ DOM readyï¼Android 6 æäº WebView å¯è½æ²¡æ $ï¼ if (typeof $ !== 'undefined') { $(document).ready(onReady); } else if (document.readyState === 'complete' || document.readyState === 'interactive') { setTimeout(onReady, 1); } else { document.addEventListener('DOMContentLoaded', onReady); } </script> </body> </html> big¹ö¶¯.html
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,823 @@ <!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, target-densitydpi=device-dpi" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <title>大å </title> <style> * { margin: 0; padding: 0; box-sizing: border-box; /* Android 6 å¯ç¨ä¸æåä½ */; font-family: "Droid Sans Fallback", "Noto Sans CJK SC", "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif; } /* 1. å ¨å±æ·±è²èæ¯ */ body { background: #001f3f; color: #fff; height: 100vh; overflow: hidden; display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-orient: vertical; -webkit-box-direction: normal; -webkit-flex-direction: column; flex-direction: column; font-size: 14px; } /* 2. 顶鍿 */ .top-header { display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-align: center; -webkit-align-items: center; align-items: center; padding: 15px 30px; background: rgba(0, 30, 60, 0.9); white-space: nowrap; position: relative; border-bottom: 1px solid #003366; } .top-header img { height: 50px; margin-right: 20px; z-index: 2; } .top-header .title-text { position: absolute; left: 50%; -webkit-transform: translateX(-50%); transform: translateX(-50%); font-size: 32px; font-weight: bold; color: #fff; z-index: 1; } .top-header .time-info { font-size: 20px; color: #aaa; margin-left: auto; z-index: 2; text-align: right; line-height: 1.3; } .time-info-inner { display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-align: center; -webkit-align-items: center; align-items: center; } .time-left { display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-orient: vertical; -webkit-flex-direction: column; flex-direction: column; text-align: right; line-height: 1.3; } .time-right { margin-left: 8px; } .top-header .time-info .time-clock { font-size: 36px; font-weight: bold; color: #ffcc00; line-height: 1.1; } /* 3. 主ä½å 容ï¼gap æ¿æ¢ä¸º margin å ¼å®¹æ§ Chromeï¼ */ .main-content { -webkit-box-flex: 1; -webkit-flex: 1; flex: 1; display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-orient: vertical; -webkit-box-direction: normal; -webkit-flex-direction: column; flex-direction: column; padding: 15px 20px; overflow: hidden; } /* åè¡å®¹å¨ */ .columns-row { -webkit-box-flex: 1; -webkit-flex: 1; flex: 1; display: -webkit-box; display: -webkit-flex; display: flex; overflow: hidden; } .columns-row > .column-box { margin-left: 5px; margin-right: 5px; } .columns-row > .column-box:first-child { margin-left: 0; } .columns-row > .column-box:last-child { margin-right: 0; } /* åå®¹å¨ */ .column-box { display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-orient: vertical; -webkit-box-direction: normal; -webkit-flex-direction: column; flex-direction: column; background: rgba(10, 40, 80, 0.5); border-radius: 0; padding: 10px; -webkit-box-flex: 1; -webkit-flex: 1; flex: 1; min-width: 0; border: 1px solid #003366; } .column-box.col-wide { -webkit-box-flex: 2.5; -webkit-flex: 2.5; flex: 2.5; } .column-box.col-normal { -webkit-box-flex: 1; -webkit-flex: 1; flex: 1; } /* æ é¢è¡ */ .col-title-line { font-size: 22px; font-weight: bold; color: #4da6ff; text-align: center; } .col-title { padding-bottom: 8px; border-bottom: 1px solid #003366; margin-bottom: 10px; text-align: center; } .col-subtitle { font-size: 14px; color: #999; text-align: center; min-height: 20px; /* å³ä½¿ä¸ºç©ºä¹ä¿çé«åº¦ï¼é²æ¢æ 颿 é«ä½ä¸å¹³ */; line-height: 1.4; word-break: keep-all; } /* æ£è å表 */ .patient-list { -webkit-box-flex: 1; -webkit-flex: 1; flex: 1; overflow-y: auto; padding-right: 5px; } /* 第1æ ï¼ä¸è¡ä¸¤ä¸ª */ #col-0 { display: -webkit-box; display: -webkit-flex; display: flex; -webkit-flex-wrap: wrap; flex-wrap: wrap; -webkit-align-content: flex-start; align-content: flex-start; } .col-normal .patient-list { display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-orient: vertical; -webkit-box-direction: normal; -webkit-flex-direction: column; flex-direction: column; } /* æ£è é¡¹ç® */ .patient-item { font-size: 22px; padding: 8px 5px; display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-align: center; -webkit-align-items: center; align-items: center; color: #fff; line-height: 1.4; } #col-0 .patient-item { width: 45%; font-size: 20px; margin: 0 2.5%; } .p-number { color: #ffcc00; font-weight: bold; margin-right: 8px; } .p-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .p-name-wrap { -webkit-box-flex: 1; -webkit-flex: 1; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .p-room { color: #4da6ff; font-weight: bold; font-size: 18px; margin-left: 4px; } /* è¿å·æ ï¼åºå®å¨ main-content åºé¨ï¼èªå¨æ»å¨ï¼ */ .missed-bar { -webkit-flex-shrink: 0; flex-shrink: 0; display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-align: center; -webkit-align-items: center; align-items: center; background: rgba(80, 20, 20, 0.7); border: 1px solid #993333; padding: 6px 10px; margin-top: 8px; } .missed-bar-empty { display: none; } .missed-bar-title { -webkit-flex-shrink: 0; flex-shrink: 0; color: #ff6666; font-size: 18px; font-weight: bold; margin-right: 16px; line-height: 30px; } /* æ»å¨è£åªåº */ .missed-bar-scroll-area { -webkit-box-flex: 1; -webkit-flex: 1; flex: 1; overflow: hidden; white-space: nowrap; } /* æ»å¨è½¨é */ .missed-bar-track { display: inline-block; white-space: nowrap; will-change: transform; } .missed-bar-scroll .missed-bar-track { /* å¨ç»æ¶é¿ç± JS å¨æè®¾ç½®ï¼è§ updateMissedBarSpeed */ -webkit-animation-name: missed-scroll; animation-name: missed-scroll; -webkit-animation-timing-function: linear; animation-timing-function: linear; -webkit-animation-iteration-count: infinite; animation-iteration-count: infinite; } @-webkit-keyframes missed-scroll { 0% { -webkit-transform: translateX(0); transform: translateX(0); } 100% { -webkit-transform: translateX(-50%); transform: translateX(-50%); } } @keyframes missed-scroll { 0% { transform: translateX(0); } 100% { transform: translateX(-50%); } } .missed-bar .patient-item { display: -webkit-inline-box; display: -webkit-inline-flex; display: inline-flex; font-size: 17px; padding: 3px 12px; margin-right: 8px; border-right: 1px solid #664444; color: #cc9999; } .missed-bar .patient-item:last-child { border-right: none; } .missed-bar .p-number { color: #ff9966; font-weight: bold; margin-right: 6px; } .missed-bar .p-name { color: #bbaaaa; } .missed-bar .p-room { color: #ff6666; font-size: 15px; } /* åºé¨æ */ .bottom-footer { display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-pack: center; -webkit-justify-content: center; justify-content: center; -webkit-box-align: center; -webkit-align-items: center; align-items: center; padding: 10px 30px; background: rgba(0, 30, 60, 0.9); border-top: 1px solid #003366; } .footer-tip { font-size: 22px; color: #ffcc00; font-weight: bold; text-align: center; } /* æ»å¨æ¡ */ .patient-list::-webkit-scrollbar { width: 4px; } .patient-list::-webkit-scrollbar-thumb { background: #444; border-radius: 2px; } /* è°è¯é¢æ¿ */ .debug-panel { position: fixed; right: 15px; bottom: 70px; width: 350px; height: 200px; background: rgba(0, 0, 0, 0.9); color: #0f0; border-radius: 4px; font-family: monospace; font-size: 12px; z-index: 9999; overflow: hidden; } .debug-header { display: -webkit-box; display: -webkit-flex; display: flex; -webkit-box-pack: justify; -webkit-justify-content: space-between; justify-content: space-between; -webkit-box-align: center; -webkit-align-items: center; align-items: center; padding: 5px; background: #333; border-bottom: 1px solid #000; } .debug-header span { font-weight: bold; font-size: 13px; } .debug-header button { background: #c00; color: #fff; border: none; padding: 2px 6px; font-size: 10px; cursor: pointer; } .debug-body { -webkit-box-flex: 1; -webkit-flex: 1; flex: 1; padding: 5px; overflow-y: auto; font-size: 11px; } .debug-line { margin-bottom: 2px; } .debug-time { color: #888; } </style> </head> <body> <!-- 顶鍿 --> <div class="top-header"> <img src="logo.png" alt="logo" /> <span class="title-text">å¿çµæ£æ¥æå¡è¯åºå¤§å </span> <span class="time-info" id="headerTime"></span> </div> <!-- 主ä½å 容 --> <div class="main-content" id="mainContent"></div> <!-- åºé¨æ --> <div class="bottom-footer"> <div class="footer-tip">温馨æç¤ºï¼è¯·å¬å°å¼å«ååå¾å¯¹åºè¯å®¤ï¼è¿å·æ£è 请å°å¯¼è¯å°å¤çï¼</div> </div> <!-- è°è¯é¢æ¿ --> <div class="debug-panel" id="debugPanel"> <div class="debug-header"> <span>[è°è¯] è¿è¡æ¥å¿</span> <button onclick="document.getElementById('debugBody').innerHTML=''">æ¸ ç©º</button> </div> <div class="debug-body" id="debugBody"></div> </div> <script src="./static/jquery.min.js"></script> <script> // ================= è°è¯æ¥å¿ ================= function logDebug(msg) { var now = new Date(); function pad(num) { return num < 10 ? '0' + num : '' + num; } var timeStr = pad(now.getHours()) + ':' + pad(now.getMinutes()) + ':' + pad(now.getSeconds()); var logHtml = '<div class="debug-line"><span class="debug-time">[' + timeStr + ']</span> ' + msg + '</div>'; console.log("[" + timeStr + "] " + msg); var body = document.getElementById("debugBody"); if (body) { body.insertAdjacentHTML("beforeend", logHtml); body.scrollTop = body.scrollHeight; } } // ================= åºç¨ç¶æ ================= var appState = { columnTitles: ["常è§å¿çµå¾", "卿å¿çµå¾/è¡å", "å¿çµå¾è¿å¨è¯éª", "é£éçµçç", "å¨è硬åçæµ"], columnSubTitles: ["åºè¾¹å¿çµå¾(常è§+é¢è°±) / å¿çµåéå¾", "", "", "", ""], patients: [], apiBaseUrl: "http://localhost/admin-api", // apiBaseUrl: "http://192.168.100.110/admin-api", pollTimer: null, callRepeatTimes: 2, spokenPatients: {}, ttsQueue: [], isSpeaking: false, // è¿å·æ£è æ»å¨é度é ç½®ï¼æ°å¼ä¸ºå¨ç»å¨æç§æ°ï¼è¶å¤§è¶æ ¢ missedScrollSpeedSlowest: 10, // ææ ¢éåº¦ï¼æ£è å°æ¶ä½¿ç¨ï¼ç§æ°å¤§=æ ¢ï¼ missedScrollSpeedFastest: 5 // æå¿«éåº¦ï¼æ£è 夿¶ä¸éï¼ç§æ°å°=å¿«ï¼ }; // ================= è¯é³ææ¥ ================= function processTtsQueue() { if (appState.isSpeaking || appState.ttsQueue.length === 0) return; appState.isSpeaking = true; var text = appState.ttsQueue.shift(); logDebug("[ææ¥] " + text); var utterance = new SpeechSynthesisUtterance(text); // Android 6 é»è®¤è¯éå¯è½å¾å¿«ï¼éå½è°æ ¢ utterance.rate = 0.85; utterance.onend = function () { appState.isSpeaking = false; processTtsQueue(); }; utterance.onerror = function (e) { logDebug("[TTSé误] " + (e.error || "unknown")); appState.isSpeaking = false; processTtsQueue(); }; // ä¼å å°è¯è®¾å¤åç TTS æ¥å£ if (typeof wowjoy !== 'undefined' && typeof wowjoy.speek === 'function') { try { wowjoy.speek(text); setTimeout(function () { appState.isSpeaking = false; processTtsQueue(); }, 4000); return; } catch (e) { logDebug("[wowjoy失败] " + e.message); } } if (window.speechSynthesis) { // Android 6 WebView ææ¶éè¦å cancel å speak window.speechSynthesis.cancel(); window.speechSynthesis.speak(utterance); } else { logDebug("[TTS] æµè§å¨ä¸æ¯æè¯é³åæ"); appState.isSpeaking = false; processTtsQueue(); } } function speak(text) { if (!text) return; appState.ttsQueue.push(text); processTtsQueue(); } // ================= 渲æï¼æ 颿¡æ¶ï¼ä» åå§å䏿¬¡ï¼================= function initLayout() { var main = document.getElementById("mainContent"); var html = '<div class="columns-row">'; for (var i = 0; i < appState.columnTitles.length; i++) { // æ é¢å坿 é¢é½æåºå®å ä½ï¼é¿å æ/æ 坿 颿¶é«åº¦è·³å¨ var subtitle = appState.columnSubTitles[i] || " "; var colClass = (i === 0) ? 'col-wide' : 'col-normal'; html += '<div class="column-box ' + colClass + '">' + '<div class="col-title">' + '<div class="col-title-line">' + appState.columnTitles[i] + '</div>' + '<div class="col-subtitle">' + subtitle + '</div>' + '</div>' + '<div class="patient-list" id="col-' + i + '"></div>' + '</div>'; } html += '</div>'; html += '<div class="missed-bar missed-bar-empty" id="missedBar">' + '<span class="missed-bar-title">è¿å·æ£è </span>' + '<div class="missed-bar-scroll-area"><div class="missed-bar-track" id="missedBarTrack"></div></div>' + '</div>'; main.innerHTML = html; } // ================= 渲æï¼æ£è å表ï¼diff æ´æ°ï¼åªæ¹ååçåï¼================= function desensitizeName(name) { if (!name || name.length < 2) return name || ''; return name.charAt(0) + '*' + name.substring(2); } function isMissedStatus(status) { var s = parseInt(status, 10); return s === 3 || s === 5 || s === 7; } function buildColumnHtml(colData) { if (!Array.isArray(colData) || colData.length === 0) return ""; var h = ""; for (var p = 0; p < colData.length; p++) { var pat = colData[p]; // è¿å·æ£è 䏿¾ç¤ºå¨åå ï¼ç»ä¸å¨åºé¨è¿å·æ å±ç¤º if (isMissedStatus(pat.status)) continue; var roomHtml = pat.roomName ? '<span class="p-room">(' + pat.roomName + ')</span>' : ''; h += '<div class="patient-item">' + '<span class="p-number">' + (pat.seqNum || '') + '</span>' + '<span class="p-name-wrap"><span class="p-name">' + desensitizeName(pat.patName) + '</span>' + roomHtml + '</span>' + '</div>'; } return h; } function buildMissedBarHtml(dataList) { var allMissed = []; for (var i = 0; i < appState.columnTitles.length; i++) { var colData = (dataList && dataList[i]) ? dataList[i] : []; for (var p = 0; p < colData.length; p++) { var pat = colData[p]; if (isMissedStatus(pat.status)) { allMissed.push({ patient: pat, colIndex: i }); } } } if (allMissed.length === 0) return ""; var h = ""; for (var m = 0; m < allMissed.length; m++) { var item = allMissed[m]; var pat = item.patient; var colName = appState.columnTitles[item.colIndex]; var roomHtml = pat.roomName ? ' <span class="p-room">' + pat.roomName + '</span>' : ''; h += '<span class="patient-item">' + '<span class="p-number">[' + colName + '] ' + (pat.seqNum || '') + '</span>' + '<span class="p-name">' + desensitizeName(pat.patName) + '</span>' + roomHtml + '</span>'; } // å¤å¶ä¸ä»½å 容å®ç°æ ç¼å¾ªç¯æ»å¨ return h + h; } function isSameColumn(a, b) { if (!Array.isArray(a) || !Array.isArray(b)) return a === b; if (a.length !== b.length) return false; for (var i = 0; i < a.length; i++) { if (a[i].patId !== b[i].patId) return false; if (a[i].roomName !== b[i].roomName) return false; if (parseInt(a[i].status, 10) !== parseInt(b[i].status, 10)) return false; } return true; } function updatePatients(dataList) { for (var i = 0; i < appState.columnTitles.length; i++) { var newData = (dataList && dataList[i]) ? dataList[i] : []; var oldData = appState.patients[i]; // diffï¼æ°æ®æªåååè·³è¿è¯¥å DOM æä½ if (isSameColumn(oldData, newData)) continue; var col = document.getElementById("col-" + i); if (col) col.innerHTML = buildColumnHtml(newData); } // æ´æ°åºé¨è¿å·æ var missedBar = document.getElementById("missedBar"); var missedTrack = document.getElementById("missedBarTrack"); if (missedBar && missedTrack) { var missedHtml = buildMissedBarHtml(dataList); if (missedHtml) { missedTrack.innerHTML = missedHtml; missedBar.className = "missed-bar missed-bar-scroll"; // æ ¹æ®è¿å·æ£è 人æ°å¨æè°æ´æ»å¨é度 updateMissedBarSpeed(missedTrack, dataList); } else { missedBar.className = "missed-bar missed-bar-empty"; } } } // ================= è¿å·æ æ»å¨é度 ================= // ç»è®¡è¿å·æ£è æ»æ°ï¼å¨ 0 å° 20 人ä¹é´çº¿æ§æå¼å¨ç»æ¶é¿ // äººå°æ¶ç¨ææ ¢é度ï¼ç§æ°å¤§ï¼ï¼äººå¤æ¶é¼è¿æå¿«é度ï¼ç§æ°å°ï¼ï¼æå¤ 20 人达å°ä¸é function updateMissedBarSpeed(trackEl, dataList) { var count = 0; for (var i = 0; i < appState.columnTitles.length; i++) { var colData = (dataList && dataList[i]) ? dataList[i] : []; for (var p = 0; p < colData.length; p++) { if (isMissedStatus(colData[p].status)) count++; } } if (count === 0) return; var slowest = appState.missedScrollSpeedSlowest; var fastest = appState.missedScrollSpeedFastest; // 人æ°éå¼ï¼è¶ è¿æ¤äººæ°å³ä½¿ç¨æå¿«é度 var maxCount = 20; // çº¿æ§æå¼ï¼count=1 -> slowest, count>=maxCount -> fastest var ratio = Math.min(count, maxCount) / maxCount; var duration = slowest - (slowest - fastest) * ratio; // åæ´å°ä¸ä½å°æ°ï¼å ¼å ·ç²¾åº¦åç®æ´ duration = Math.round(duration * 10) / 10; var animValue = 'missed-scroll ' + duration + 's linear infinite'; trackEl.style.webkitAnimation = animValue; trackEl.style.animation = animValue; } // ================= æ°æ®å¯¹æ¯ä¸ææ¥ ================= function checkAndSpeakNewRooms(oldData, newData) { if (!oldData || oldData.length === 0) return; for (var c = 0; c < newData.length; c++) { var newCol = newData[c]; var oldCol = oldData[c]; if (Array.isArray(newCol)) { for (var p = 0; p < newCol.length; p++) { var newPat = newCol[p]; if (newPat.roomName) { if (appState.spokenPatients[newPat.patId]) continue; var oldPat = null; if (Array.isArray(oldCol)) { for (var k = 0; k < oldCol.length; k++) { if (oldCol[k].patId === newPat.patId) { oldPat = oldCol[k]; break; } } } if (!oldPat || !oldPat.roomName) { var repeatText = ""; for (var r = 0; r < appState.callRepeatTimes; r++) { repeatText += "请 " + newPat.seqNum + " å· " + newPat.patName + " å° " + newPat.roomName + " å°±è¯ã"; } speak(repeatText); appState.spokenPatients[newPat.patId] = true; logDebug("[æé] " + newPat.patName + " -> " + newPat.roomName); } } } } } } // ================= æ°æ®è·å ================= function fetchQueueData() { var url = appState.apiBaseUrl + "/ecg/screen/big-screen-data"; $.ajax({ url: url, type: "GET", dataType: "json", timeout: 5000, success: function (res) { var dataList = []; if (res && res.code === 0 && res.data) { for (var i = 0; i < appState.columnTitles.length; i++) { var key = i.toString(); dataList[i] = (res.data[key] && Array.isArray(res.data[key])) ? res.data[key] : []; } } // å diff æ´æ° DOMï¼åææ¥ï¼æåä¿åæ°æ®ç¨äºä¸æ¬¡å¯¹æ¯ updatePatients(dataList); checkAndSpeakNewRooms(appState.patients, dataList); appState.patients = dataList; // Android 6 WebView é叏䏿¯æ performance.memoryï¼å®å ¨å®å« try { if (window.performance && window.performance.memory) { var usedMB = (window.performance.memory.usedJSHeapSize / 1048576).toFixed(2); logDebug("[å å] " + usedMB + " MB"); } } catch (e) {} }, error: function (err) { logDebug("[请æ±å¤±è´¥] " + (err.statusText || "ç½ç»é误")); } }); } // ================= åå§å ================= function onReady() { initLayout(); updateHeaderTime(); setInterval(updateHeaderTime, 1000); fetchQueueData(); appState.pollTimer = setInterval(fetchQueueData, 5000); logDebug("[ç³»ç»] å¯å¨å®æ - Android 6.0.1"); } function updateHeaderTime() { var now = new Date(); var weekDays = ["æææ¥", "ææä¸", "ææäº", "ææä¸", "ææå", "ææäº", "ææå "]; function padZero(num) { return num < 10 ? '0' + num : '' + num; } var dateStr = now.getFullYear() + "å¹´" + padZero(now.getMonth() + 1) + "æ" + padZero(now.getDate()) + "æ¥"; var weekStr = weekDays[now.getDay()]; var timeStr = padZero(now.getHours()) + ":" + padZero(now.getMinutes()); var el = document.getElementById("headerTime"); // ææåæ¥æåå ä¸è¡å¨å·¦ï¼æ¶é´å¤§åå¨å³è·¨ä¸¤è¡ if (el) el.innerHTML = '<div class="time-info-inner"><div class="time-left"><div>' + weekStr + '</div><div>' + dateStr + '</div></div><div class="time-right"><span class="time-clock">' + timeStr + '</span></div></div>'; } // å ¼å®¹ DOM readyï¼Android 6 æäº WebView å¯è½æ²¡æ $ï¼ if (typeof $ !== 'undefined') { $(document).ready(onReady); } else if (document.readyState === 'complete' || document.readyState === 'interactive') { setTimeout(onReady, 1); } else { document.addEventListener('DOMContentLoaded', onReady); } </script> </body> </html> logo0.png
small.html
@@ -1,4 +1,4 @@ <!DOCTYPE html> <!DOCTYPE html> <html lang="zh-CN"> <head> @@ -36,8 +36,7 @@ .header-top { display: flex; align-items: center; justify-content: center; gap: 30px; justify-content: space-between; } .logo { @@ -48,13 +47,19 @@ 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 { @@ -87,22 +92,35 @@ 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; } @@ -134,27 +152,33 @@ /* æ£è ä¿¡æ¯é¡¹ (åºå®é«åº¦ï¼ä¸è¡ä¸¤ä¸ª) */ .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; } /* ================= åºé¨æ§å¶åº ================= */ @@ -177,6 +201,7 @@ } #test-voice-btn { display: none; position: absolute; right: 20px; background-color: #007bff; @@ -195,6 +220,7 @@ /* è°è¯ä¿¡æ¯åº */ .debug-info { display: none; font-size: 14px; color: #999; background: #f0f0f0; @@ -222,11 +248,13 @@ <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> @@ -234,19 +262,19 @@ <!-- 主ä½å 容 --> <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> @@ -254,7 +282,7 @@ <!-- åºé¨æ§å¶æ --> <div class="footer"> <span class="footer-text">温馨æç¤ºï¼è¯·èå¿çå¾ ï¼ä¿æå®éï¼</span> <span class="footer-text">温馨æç¤ºï¼è¯·è¿å·æ£è å°åè¯å°å¤çï¼</span> <button id="test-voice-btn">æµè¯è¯é³</button> </div> @@ -262,63 +290,130 @@ <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. æ´æ°è¯å®¤åç§° (ä»ç¬¬ä¸æ¡æ°æ®ä¸è·å) @@ -330,6 +425,9 @@ 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; @@ -375,7 +473,7 @@ 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; @@ -383,19 +481,24 @@ } // ================= 渲æå表 ================= 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); } }