48f1db3a38e6956ee60055744481f3b72a5e607c..796047fbe84d51816f44be535501415d3c66dd9d
5 天以前 yxh
yxh
796047 对比 | 目录
2026-06-16 yxh
yxh
0450b2 对比 | 目录
已修改3个文件
已添加3个文件
2383 ■■■■■ 文件已修改
.vscode/launch.json 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
big.html 689 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
big1.html 680 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
big滚动.html 823 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
logo0.png 补丁 | 查看 | 原始文档 | blame | 历史
small.html 189 ●●●● 补丁 | 查看 | 原始文档 | 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] || "&nbsp;";
                // ä¸€è¡Œä¸¤ä¸ªçš„列用 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] || "&nbsp;";
                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] || "&nbsp;";
                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>&nbsp;<span class="p-name">' + (item.patName ? desensitizeName(item.patName) : '未知') + '</span>';
                container.appendChild(div);
            }
        }