<!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: 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: 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-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;
|
text-align: center;
|
}
|
|
.col-subtitle {
|
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: 2px;
|
display: -webkit-box;
|
display: -webkit-flex;
|
display: flex;
|
-webkit-align-content: flex-start;
|
align-content: flex-start;
|
}
|
|
/* 一行两个(常规心电图、动态心电) */
|
.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: 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;
|
}
|
|
.p-number {
|
color: #ffcc00;
|
font-weight: bold;
|
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;
|
white-space: nowrap;
|
}
|
|
.p-room {
|
color: #4da6ff;
|
font-weight: bold;
|
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;
|
}
|
|
/* 底部栏 */
|
.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;
|
}
|
|
/* 测试语音按钮 */
|
#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;
|
}
|
|
/* 调试面板 */
|
.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: 380px;
|
height: 220px;
|
background: rgba(0, 0, 0, 0.92);
|
color: #0f0;
|
border-radius: 6px;
|
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>
|
<button id="test-voice-btn" onclick="testVoice()">测试语音</button>
|
</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);
|
// 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: ["1号-2号诊室", "2号诊室", "4号诊室", "6号诊室", "5号诊室"],
|
patients: [],
|
// apiBaseUrl: "http://192.168.3.12/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: "已就诊"
|
};
|
|
// ================= 语音播报 =================
|
// 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);
|
|
// 优先尝试设备原生 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) {
|
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) {
|
if (!text) return;
|
appState.ttsQueue.push(text);
|
processTtsQueue();
|
}
|
|
// ================= 语音测试 =================
|
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");
|
if (!main) { logDebug("[布局] mainContent 元素未找到!"); return; }
|
var html = '<div class="columns-row">';
|
var visibleCount = 0;
|
|
for (var i = 0; i < appState.columnTitles.length; i++) {
|
// 食道电生理:通过 SHOW_COL_ESOPHAGEAL 控制显隐
|
if (i === 3 && !SHOW_COL_ESOPHAGEAL) {
|
logDebug("[布局] 列" + i + " 食道电生理 已隐藏");
|
continue;
|
}
|
|
var subtitle = appState.columnSubTitles[i] || " ";
|
// 一行两个的列用 col-wide,否则 col-normal
|
var isTwoPerRow = TWO_PER_ROW_COLS.indexOf(i) !== -1;
|
var colClass = isTwoPerRow ? 'col-wide' : 'col-normal';
|
var rowClass = isTwoPerRow ? 'two-per-row' : 'one-per-row';
|
logDebug("[布局] 列" + i + " " + appState.columnTitles[i] + " class=" + colClass + " row=" + rowClass);
|
html += '<div class="column-box ' + colClass + '">' +
|
'<div class="col-title">' +
|
'<div class="col-title-line">' + appState.columnTitles[i] + '</div>' +
|
'<div class="col-subtitle">' + subtitle + '</div>' +
|
'</div>' +
|
'<div class="patient-list ' + rowClass + '" id="col-' + i + '"></div>' +
|
'</div>';
|
visibleCount++;
|
}
|
html += '</div>';
|
main.innerHTML = html;
|
logDebug("[布局] initLayout 完成, 可见列=" + visibleCount);
|
}
|
|
// ================= 渲染:患者列表(diff 更新,只改变化的列)=================
|
function desensitizeName(name) {
|
if (!name || name.length < 2) return name || '';
|
return name.charAt(0) + '*' + name.substring(2);
|
}
|
|
function isMissedStatus(status) {
|
var s = parseInt(status, 10);
|
return s === 3 || s === 5 || s === 7;
|
}
|
|
function buildColumnHtml(colData) {
|
if (!Array.isArray(colData) || colData.length === 0) return "";
|
var h = "";
|
for (var p = 0; p < colData.length; p++) {
|
var pat = colData[p];
|
var missedClass = isMissedStatus(pat.status) ? ' missed' : '';
|
var roomHtml = (!isMissedStatus(pat.status) && pat.roomName) ? '<span class="p-room">' + pat.roomName + '</span>' : '';
|
h += '<div class="patient-item' + missedClass + '">' +
|
'<span class="p-number">' + (pat.seqNum || '') + '</span>' +
|
'<span class="p-name-wrap"><span class="p-name">' + desensitizeName(pat.patName) + '</span>' + roomHtml +
|
(missedClass ? '<span class="p-missed-tag">过号</span>' : '') + '</span>' +
|
'</div>';
|
}
|
return h;
|
}
|
function isSameColumn(a, b) {
|
if (!Array.isArray(a) || !Array.isArray(b)) return a === b;
|
if (a.length !== b.length) return false;
|
for (var i = 0; i < a.length; i++) {
|
if (a[i].patId !== b[i].patId) return false;
|
if (a[i].roomName !== b[i].roomName) return false;
|
if (parseInt(a[i].status, 10) !== parseInt(b[i].status, 10)) return false;
|
}
|
return true;
|
}
|
|
function updatePatients(dataList) {
|
for (var i = 0; i < appState.columnTitles.length; i++) {
|
var newData = (dataList && dataList[i]) ? dataList[i] : [];
|
var oldData = appState.patients[i];
|
|
// diff:数据未变化则跳过该列 DOM 操作
|
if (isSameColumn(oldData, newData)) continue;
|
|
var col = document.getElementById("col-" + i);
|
if (col) {
|
col.innerHTML = buildColumnHtml(newData);
|
logDebug("[更新] 列" + i + " " + appState.columnTitles[i] + " 刷新 " + newData.length + " 人");
|
} else {
|
logDebug("[更新] 列" + i + " DOM不存在(可能已隐藏)");
|
}
|
}
|
}
|
|
// ================= 数据对比与播报 =================
|
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_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";
|
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;
|
|
// 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("[请求失败] status=" + err.status + " " + (err.statusText || "网络错误") + " url=" + url);
|
}
|
});
|
}
|
|
// ================= 初始化 =================
|
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);
|
try {
|
fetchQueueData();
|
logDebug("[初始化] 首次 fetchQueueData 已发起");
|
} catch (e) { logDebug("[初始化] fetchQueueData 异常: " + e.message); }
|
appState.pollTimer = setInterval(fetchQueueData, 5000);
|
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()) + "日";
|
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 可能没有 $)
|
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>
|
|
</html>
|