yxh
yxh
5 天以前 796047fbe84d51816f44be535501415d3c66dd9d
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, target-densitydpi=device-dpi" />
    <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,7 +12,8 @@
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            /* Android 6 可用中文字体 */;
            /* Android 6 可用中文字体 */
            ;
            font-family: "Droid Sans Fallback", "Noto Sans CJK SC", "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
        }
@@ -64,10 +66,43 @@
        }
        .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;
        }
        .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) */
@@ -78,20 +113,35 @@
            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;
        }
        .main-content > .column-box {
            margin-left: 5px;
            margin-right: 5px;
        /* 列行容器 */
        .columns-row {
            -webkit-box-flex: 1;
            -webkit-flex: 1;
            flex: 1;
            display: -webkit-box;
            display: -webkit-flex;
            display: flex;
            overflow: hidden;
        }
        .main-content > .column-box:first-child {
        .columns-row>.column-box {
            margin-left: 3px;
            margin-right: 3px;
        }
        .columns-row>.column-box:first-child {
            margin-left: 0;
        }
        .main-content > .column-box:last-child {
        .columns-row>.column-box:last-child {
            margin-right: 0;
        }
@@ -106,7 +156,7 @@
            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;
@@ -128,16 +178,13 @@
        /* 标题行 */
        .col-title-line {
            font-size: 22px;
            font-size: 28px;
            font-weight: bold;
            color: #4da6ff;
            text-align: center;
        }
        .col-title {
            font-size: 22px;
            font-weight: bold;
            color: #4da6ff;
            padding-bottom: 8px;
            border-bottom: 1px solid #003366;
            margin-bottom: 10px;
@@ -145,9 +192,14 @@
        }
        .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;
        }
        /* 患者列表 */
@@ -156,34 +208,41 @@
            -webkit-flex: 1;
            flex: 1;
            overflow-y: auto;
            padding-right: 5px;
        }
        /* 第1栏:一行两个 */
        #col-0 {
            padding-right: 2px;
            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;
        /* 一行两个(常规心电图、动态心电) */
        .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;
        }
        .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;
@@ -194,19 +253,22 @@
            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;
            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;
@@ -218,8 +280,31 @@
        .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;
        }
        /* 过号患者:排在列内,视觉弱化 */
        .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;
        }
        /* 底部栏 */
@@ -245,10 +330,30 @@
            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;
        }
        .patient-list::-webkit-scrollbar-thumb {
            background: #444;
            border-radius: 2px;
@@ -256,14 +361,21 @@
        /* 调试面板 */
        .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;
@@ -322,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>
@@ -331,7 +443,8 @@
    <!-- 底部栏 -->
    <div class="bottom-footer">
        <div class="footer-tip">温馨提示:请听到呼叫后前往对应诊室</div>
        <div class="footer-tip">温馨提示:听到呼叫后请前往对应诊室,过号患者到导诊台处理!</div>
        <button id="test-voice-btn" onclick="testVoice()">测试语音</button>
    </div>
    <!-- 调试面板 -->
@@ -355,61 +468,125 @@
            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.100.110/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);
            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 接口
            // 优先尝试设备原生 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); }
            }
            if (window.speechSynthesis) {
                // Android 6 WebView 有时需要先 cancel 再 speak
                window.speechSynthesis.cancel();
                window.speechSynthesis.speak(utterance);
            } else {
            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) {
@@ -418,47 +595,179 @@
            processTtsQueue();
        }
        // ================= 渲染 =================
        function renderMainContent() {
        // ================= 语音测试 =================
        function testVoice() {
            logDebug("[语音测试] ========== 开始 ==========");
            var testText = "测试语音播报";
            // 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);
                }
                // 如果 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 initLayout() {
            logDebug("[布局] initLayout 开始, 列数=" + appState.columnTitles.length);
            var main = document.getElementById("mainContent");
            main.innerHTML = "";
            if (!main) { logDebug("[布局] mainContent 元素未找到!"); return; }
            var html = '<div class="columns-row">';
            var visibleCount = 0;
            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>';
                // 食道电生理:通过 SHOW_COL_ESOPHAGEAL 控制显隐
                if (i === 3 && !SHOW_COL_ESOPHAGEAL) {
                    logDebug("[布局] 列" + i + " 食道电生理 已隐藏");
                    continue;
                }
                var colClass = (i === 0) ? 'col-wide' : 'col-normal';
                var colHtml = '<div class="column-box ' + colClass + '">' +
                    '<div class="col-title">' + titleHtml + '</div>' +
                    '<div class="patient-list" id="col-' + i + '"></div>' +
                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>';
                main.insertAdjacentHTML("beforeend", colHtml);
                visibleCount++;
            }
            html += '</div>';
            main.innerHTML = html;
            logDebug("[布局] initLayout 完成, 可见列=" + visibleCount);
        }
        function renderPatients() {
            for (var i = 0; i < appState.columnTitles.length; i++) {
                var col = document.getElementById("col-" + i);
                if (col) col.innerHTML = "";
        // ================= 渲染:患者列表(diff 更新,只改变化的列)=================
        function desensitizeName(name) {
            if (!name || name.length < 2) return name || '';
            return name.charAt(0) + '*' + name.substring(2);
            }
            for (var c = 0; c < appState.patients.length; c++) {
                var colData = appState.patients[c];
                if (Array.isArray(colData)) {
                    var col = document.getElementById("col-" + c);
                    if (!col) continue;
        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 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 +
                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>';
                        col.insertAdjacentHTML("beforeend", itemHtml);
                    }
            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不存在(可能已隐藏)");
                }
            }
        }
@@ -485,10 +794,10 @@
                            }
                            if (!oldPat || !oldPat.roomName) {
                                var repeatText = "";
                                for (var r = 0; r < appState.callRepeatTimes; r++) {
                                    repeatText += "请 " + newPat.bookSeqNum + " 号 " + newPat.patName + " 到 " + newPat.roomName + " 就诊。";
                                }
                                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 + " -> " + newPat.roomName);
                            }
@@ -501,22 +810,28 @@
        // ================= 数据获取 =================
        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();
                    // Android 6 WebView 通常不支持 performance.memory,安全守卫
                    try {
@@ -527,37 +842,51 @@
                    } catch (e) {}
                },
                error: function (err) {
                    logDebug("[请求失败] " + (err.statusText || "网络错误"));
                    logDebug("[请求失败] status=" + err.status + " " + (err.statusText || "网络错误") + " url=" + url);
                }
            });
        }
        // ================= 初始化 =================
        function onReady() {
            renderMainContent();
            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);
            try {
            fetchQueueData();
                logDebug("[初始化] 首次 fetchQueueData 已发起");
            } catch (e) { logDebug("[初始化] fetchQueueData 异常: " + e.message); }
            appState.pollTimer = setInterval(fetchQueueData, 5000);
            logDebug("[系统] 启动完成 - Android 6.0.1");
            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());
            var el = document.getElementById("headerTime");
            if (el) el.textContent = dateStr + " " + timeStr;
            // 星期和日期各占一行在左,时间大字在右跨两行
            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>