content.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691
  1. /**
  2. * Content Script - 注入到HIS页面的脚本
  3. * 功能: 添加语音按钮、使用浏览器语音识别、调用后端AI服务、自动填表
  4. * 支持: 多页面配置、自定义提示词模板、自定义字段映射
  5. */
  6. // 配置信息
  7. const CONFIG = {
  8. // 后端服务地址 - 提供千问大模型服务
  9. BACKEND_URL: 'http://localhost:8080',
  10. API_ENDPOINT: '/api/extract',
  11. // 按钮样式配置
  12. BUTTON_STYLE: {
  13. position: 'fixed',
  14. bottom: '100px',
  15. right: '30px',
  16. zIndex: 10000,
  17. padding: '15px 20px',
  18. backgroundColor: '#4CAF50',
  19. color: 'white',
  20. border: 'none',
  21. borderRadius: '50px',
  22. cursor: 'pointer',
  23. fontSize: '16px',
  24. fontWeight: 'bold',
  25. boxShadow: '0 4px 6px rgba(0,0,0,0.1)',
  26. transition: 'all 0.3s ease'
  27. },
  28. // 录音状态指示器样式
  29. RECORDING_STYLE: {
  30. backgroundColor: '#f44336',
  31. animation: 'pulse 1.5s infinite'
  32. }
  33. };
  34. // 录音状态
  35. let isRecording = false;
  36. let recognition = null;
  37. let recordingTimer = null;
  38. let recordingSeconds = 0;
  39. const MAX_RECORDING_SECONDS = 60; // 最长录音时间 60 秒
  40. // 当前页面配置
  41. let currentPageConfig = null;
  42. // 存储已填写的字段(用于多次提交覆盖)
  43. let filledFields = {};
  44. /**
  45. * 初始化: 在页面加载完成后添加语音按钮
  46. */
  47. function init() {
  48. console.log('[医疗语音助手] init() 被调用');
  49. // 加载当前页面的配置(异步)
  50. loadPageConfig().then(config => {
  51. currentPageConfig = config;
  52. console.log('[医疗语音助手] 页面配置已加载:', config);
  53. // 添加语音按钮
  54. addVoiceButton();
  55. }).catch(error => {
  56. console.error('[医疗语音助手] 加载配置失败:', error);
  57. // 即使配置加载失败,也添加按钮(使用默认配置)
  58. addVoiceButton();
  59. });
  60. }
  61. /**
  62. * 加载当前页面的配置
  63. */
  64. async function loadPageConfig() {
  65. try {
  66. // 获取当前页面 URL
  67. const currentUrl = window.location.href;
  68. console.log('[医疗语音助手] 当前页面 URL:', currentUrl);
  69. // 从存储中读取所有配置
  70. const result = await chrome.storage.local.get('pageConfigs');
  71. const configs = result.pageConfigs || [];
  72. console.log('[医疗语音助手] 已保存的配置数量:', configs.length);
  73. // 查找匹配的配置
  74. const matchedConfig = configs.find(config => {
  75. const matched = matchUrlPattern(currentUrl, config.urlPattern);
  76. if (matched) {
  77. console.log('[医疗语音助手] 匹配到配置:', config.urlPattern);
  78. }
  79. return matched;
  80. });
  81. if (matchedConfig) {
  82. console.log('[医疗语音助手] 找到配置:', matchedConfig);
  83. return matchedConfig;
  84. }
  85. console.log('[医疗语音助手] 未找到匹配配置,使用默认配置');
  86. return null;
  87. } catch (error) {
  88. console.error('[医疗语音助手] 加载配置失败:', error);
  89. return null;
  90. }
  91. }
  92. /**
  93. * 匹配 URL 模式
  94. */
  95. function matchUrlPattern(url, pattern) {
  96. // 将通配符 * 转换为正则表达式
  97. const regexPattern = pattern
  98. .replace(/[.+?^${}()|[\]\\]/g, '\\$&') // 转义特殊字符
  99. .replace(/\*/g, '.*'); // * 转换为 .*
  100. const regex = new RegExp(`^${regexPattern}$`);
  101. return regex.test(url);
  102. }
  103. /**
  104. * 添加语音按钮到页面
  105. */
  106. function addVoiceButton() {
  107. // 检查是否已存在按钮
  108. if (document.getElementById('medical-voice-btn')) {
  109. return;
  110. }
  111. // 创建按钮
  112. const voiceButton = document.createElement('button');
  113. voiceButton.id = 'medical-voice-btn';
  114. voiceButton.innerHTML = '🎤 语音输入';
  115. Object.assign(voiceButton.style, CONFIG.BUTTON_STYLE);
  116. // 添加点击事件
  117. voiceButton.addEventListener('click', toggleRecording);
  118. // 添加到页面
  119. document.body.appendChild(voiceButton);
  120. console.log('[医疗语音助手] 按钮已添加');
  121. }
  122. /**
  123. * 切换录音状态(微信模式)
  124. * 点击开始录音,再次点击或60秒后自动结束
  125. */
  126. async function toggleRecording() {
  127. const button = document.getElementById('medical-voice-btn');
  128. if (!isRecording) {
  129. // === 开始录音 ===
  130. try {
  131. // 使用 Web Speech API 进行语音识别
  132. const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
  133. if (!SpeechRecognition) {
  134. showMessage('❌ 您的浏览器不支持语音识别');
  135. return;
  136. }
  137. recognition = new SpeechRecognition();
  138. recognition.lang = 'zh-CN';
  139. recognition.continuous = true; // 持续识别
  140. recognition.interimResults = false; // 不返回临时结果
  141. recognition.onstart = () => {
  142. console.log('[医疗语音助手] 录音已开始');
  143. isRecording = true;
  144. recordingSeconds = 0;
  145. // 更新按钮样式和文字
  146. button.innerHTML = '⏹️ 停止录音 (60s)';
  147. Object.assign(button.style, CONFIG.RECORDING_STYLE);
  148. showMessage('🎤 正在录音...请说话(再次点击按钮结束录音)');
  149. // 开始倒计时
  150. startRecordingTimer(button);
  151. };
  152. recognition.onresult = (event) => {
  153. // 获取所有识别结果
  154. let finalTranscript = '';
  155. for (let i = event.resultIndex; i < event.results.length; i++) {
  156. if (event.results[i].isFinal) {
  157. finalTranscript += event.results[i][0].transcript;
  158. }
  159. }
  160. if (finalTranscript) {
  161. console.log('[医疗语音助手] 识别结果:', finalTranscript);
  162. // 累积识别的文本(用于后续处理)
  163. if (!window.accumulatedTranscript) {
  164. window.accumulatedTranscript = '';
  165. }
  166. window.accumulatedTranscript += finalTranscript;
  167. showMessage(`🎤 正在录音...已识别 ${window.accumulatedTranscript.length} 字(${MAX_RECORDING_SECONDS - recordingSeconds}s 后自动结束)`);
  168. }
  169. };
  170. recognition.onerror = (event) => {
  171. console.error('[医疗语音助手] 语音识别错误:', event.error);
  172. let errorMsg = '语音识别失败: ' + event.error;
  173. if (event.error === 'no-speech') {
  174. errorMsg = '未检测到语音,请重试';
  175. } else if (event.error === 'audio-capture') {
  176. errorMsg = '无法访问麦克风,请检查权限';
  177. } else if (event.error === 'not-allowed') {
  178. errorMsg = '麦克风权限被拒绝';
  179. }
  180. showMessage('❌ ' + errorMsg);
  181. // 延迟重置状态
  182. setTimeout(() => {
  183. // 清除倒计时
  184. if (recordingTimer) {
  185. clearInterval(recordingTimer);
  186. recordingTimer = null;
  187. }
  188. // 重置状态
  189. isRecording = false;
  190. // 恢复按钮样式
  191. const button = document.getElementById('medical-voice-btn');
  192. if (button) {
  193. button.innerHTML = '🎤 语音输入';
  194. button.style.backgroundColor = CONFIG.BUTTON_STYLE.backgroundColor;
  195. }
  196. hideMessage();
  197. }, 2000);
  198. };
  199. recognition.onend = () => {
  200. console.log('[医疗语音助手] 语音识别结束,recordingSeconds=', recordingSeconds);
  201. console.log('[医疗语音助手] 累积识别文本长度:', window.accumulatedTranscript?.length || 0);
  202. // 清除倒计时
  203. if (recordingTimer) {
  204. clearInterval(recordingTimer);
  205. recordingTimer = null;
  206. }
  207. // 恢复按钮样式
  208. const button = document.getElementById('medical-voice-btn');
  209. if (button) {
  210. button.innerHTML = '🎤 语音输入';
  211. button.style.backgroundColor = CONFIG.BUTTON_STYLE.backgroundColor;
  212. }
  213. // 无论何种原因结束,都处理结果
  214. const transcript = window.accumulatedTranscript?.trim() || '';
  215. console.log('[医疗语音助手] 最终识别文本长度:', transcript.length);
  216. // 清空累积文本
  217. window.accumulatedTranscript = '';
  218. if (transcript) {
  219. console.log('[医疗语音助手] 准备处理识别结果');
  220. // 延迟一小段时间,确保 UI 更新完成
  221. setTimeout(() => {
  222. processText(transcript);
  223. }, 100);
  224. } else {
  225. console.log('[医疗语音助手] 无识别结果');
  226. showMessage('⚠️ 未识别到语音内容');
  227. setTimeout(() => hideMessage(), 2000);
  228. }
  229. // 重置状态
  230. isRecording = false;
  231. };
  232. // 清空累积文本
  233. window.accumulatedTranscript = '';
  234. // 开始识别
  235. recognition.start();
  236. } catch (error) {
  237. console.error('[医疗语音助手] 无法启动录音:', error);
  238. showMessage('❌ 无法启动录音: ' + error.message);
  239. setTimeout(() => hideMessage(), 3000);
  240. }
  241. } else {
  242. // === 手动停止录音 ===
  243. console.log('[医疗语音助手] 手动停止录音,请求停止识别');
  244. isRecording = false; // 先标记为停止,但不立即处理结果
  245. // 停止识别,等待 onend 事件触发后再处理结果
  246. if (recognition) {
  247. recognition.stop();
  248. }
  249. // 恢复按钮样式
  250. button.innerHTML = '🎤 语音输入';
  251. button.style.backgroundColor = CONFIG.BUTTON_STYLE.backgroundColor;
  252. showMessage('⏳ 正在处理识别结果...');
  253. }
  254. }
  255. /**
  256. * 开始录音倒计时
  257. */
  258. function startRecordingTimer(button) {
  259. recordingTimer = setInterval(() => {
  260. recordingSeconds++;
  261. const remainingSeconds = MAX_RECORDING_SECONDS - recordingSeconds;
  262. // 更新按钮倒计时
  263. button.innerHTML = `⏹️ 停止录音 (${remainingSeconds}s)`;
  264. // 达到最大时长,自动停止
  265. if (recordingSeconds >= MAX_RECORDING_SECONDS) {
  266. showMessage('⏰ 录音时间已到,正在处理...');
  267. if (recognition) {
  268. recognition.stop(); // 停止识别,会触发 onend 事件
  269. }
  270. }
  271. }, 1000);
  272. }
  273. /**
  274. * 处理文本: 调用后端AI服务 -> 结构化 -> 填表
  275. */
  276. async function processText(text) {
  277. try {
  278. // 步骤1: 调用后端千问大模型提取医疗信息
  279. showMessage('🤖 正在分析医疗信息...');
  280. const structuredData = await extractMedicalInfo(text);
  281. console.log('[医疗语音助手] 结构化数据:', structuredData);
  282. // 步骤2: 自动填表
  283. showMessage('✍️ 正在填写表单...');
  284. const filled = fillForm(structuredData);
  285. if (filled) {
  286. showMessage('✅ 填写完成!');
  287. setTimeout(() => hideMessage(), 3000);
  288. } else {
  289. showMessage('⚠️ 未找到可填写的表单字段');
  290. setTimeout(() => hideMessage(), 3000);
  291. }
  292. } catch (error) {
  293. console.error('[医疗语音助手] 处理失败:', error);
  294. showMessage('❌ 处理失败: ' + error.message);
  295. setTimeout(() => hideMessage(), 5000);
  296. }
  297. }
  298. /**
  299. * 调用后端千问大模型服务,提取医疗信息
  300. */
  301. async function extractMedicalInfo(text) {
  302. let url = CONFIG.BACKEND_URL + CONFIG.API_ENDPOINT;
  303. // 如果配置了提示词模板,添加模板参数
  304. if (currentPageConfig && currentPageConfig.promptTemplate) {
  305. url += `?templateId=${encodeURIComponent(currentPageConfig.promptTemplate)}`;
  306. }
  307. console.log('[医疗语音助手] 准备调用后端接口:', url);
  308. console.log('[医疗语音助手] 请求数据:', text);
  309. const response = await fetch(url, {
  310. method: 'POST',
  311. headers: {
  312. 'Content-Type': 'application/json'
  313. },
  314. body: text // 直接发送文本,不需要 JSON.stringify()
  315. });
  316. console.log('[医疗语音助手] 后端响应状态:', response.status);
  317. if (!response.ok) {
  318. const errorText = await response.text();
  319. console.error('[医疗语音助手] 后端错误响应:', errorText);
  320. throw new Error(`后端服务返回错误: ${response.status} - ${errorText}`);
  321. }
  322. const result = await response.json();
  323. console.log('[医疗语音助手] 后端返回数据:', result);
  324. return result;
  325. }
  326. /**
  327. * 自动填写表单(支持多次提交覆盖)
  328. */
  329. function fillForm(data) {
  330. let filledCount = 0;
  331. // 获取字段映射配置
  332. let fieldMappings;
  333. if (currentPageConfig && currentPageConfig.fieldMapping) {
  334. try {
  335. fieldMappings = JSON.parse(currentPageConfig.fieldMapping);
  336. console.log('[医疗语音助手] 使用自定义字段映射:', fieldMappings);
  337. } catch (error) {
  338. console.error('[医疗语音助手] 解析字段映射失败,使用默认映射:', error);
  339. fieldMappings = getDefaultFieldMappings();
  340. }
  341. } else {
  342. fieldMappings = getDefaultFieldMappings();
  343. }
  344. // 更新已填写的字段记录(用于后续覆盖)
  345. for (const [field, value] of Object.entries(data)) {
  346. if (value) {
  347. filledFields[field] = value;
  348. }
  349. }
  350. // 遍历所有已填写的字段(包括之前填写的)
  351. for (const [field, value] of Object.entries(filledFields)) {
  352. if (!value) continue;
  353. // 特殊处理:性别字段(单选框)
  354. if (field === 'patientGender') {
  355. const filled = fillGenderField(value, fieldMappings);
  356. if (filled) {
  357. filledCount++;
  358. console.log(`[医疗语音助手] 已填写字段: ${field} = ${value}`);
  359. }
  360. continue;
  361. }
  362. // 特殊处理:症状字段(多选框)
  363. if (field === 'symptoms' && Array.isArray(value)) {
  364. const filled = fillSymptomsField(value);
  365. if (filled) {
  366. filledCount++;
  367. console.log(`[医疗语音助手] 已填写字段: ${field} = [${value.join(', ')}]`);
  368. }
  369. continue;
  370. }
  371. // 普通字段处理(input、textarea、select)
  372. let fillValue = value;
  373. if (Array.isArray(value)) {
  374. fillValue = value.join('、');
  375. }
  376. const possibleNames = fieldMappings[field] || [];
  377. const input = findInput(possibleNames);
  378. if (input) {
  379. // 先读取当前值,避免覆盖用户手动输入的内容
  380. const currentValue = input.value || input.textContent || '';
  381. const hasExistingValue = currentValue.trim() !== '';
  382. // 如果已有值,提示用户(可选)
  383. if (hasExistingValue) {
  384. console.log(`[医疗语音助手] 覆盖字段: ${field} = "${fillValue}"(原值: "${currentValue}")`);
  385. }
  386. // 填写新值(覆盖模式)
  387. input.value = fillValue;
  388. input.dispatchEvent(new Event('input', { bubbles: true }));
  389. input.dispatchEvent(new Event('change', { bubbles: true }));
  390. filledCount++;
  391. console.log(`[医疗语音助手] 已填写字段: ${field} = ${fillValue}`);
  392. // 高亮显示填写的字段
  393. highlightField(input);
  394. }
  395. }
  396. return filledCount > 0;
  397. }
  398. /**
  399. * 获取默认字段映射
  400. */
  401. function getDefaultFieldMappings() {
  402. return {
  403. patientName: ['patientName', 'name', 'patient_name', 'xingming', '姓名', '患者姓名'],
  404. patientAge: ['patientAge', 'age', 'patient_age', 'nianling', '年龄', '患者年龄'],
  405. patientGender: ['patientGender', 'gender', 'patient_gender', 'xingbie', '性别'],
  406. patientPhone: ['patientPhone', 'phone', 'patient_phone', 'dianhua', '电话', '联系电话', '手机'],
  407. chiefComplaint: ['chiefComplaint', 'cc', 'chief_complaint', 'zhushu', '主诉'],
  408. presentIllness: ['presentIllness', 'hpi', 'present_illness', 'xianbingshi', '现病史'],
  409. pastHistory: ['pastHistory', 'ph', 'past_history', 'jiwangshi', '既往史'],
  410. allergyHistory: ['allergyHistory', 'ah', 'allergy_history', 'guominshi', '过敏史'],
  411. visitType: ['visitType', 'vt', 'visit_type', 'jiuzhenleixing', '就诊类型']
  412. };
  413. }
  414. /**
  415. * 填写性别字段(单选框)
  416. */
  417. function fillGenderField(value, fieldMappings) {
  418. // 查找所有性别单选框
  419. const possibleNames = fieldMappings?.patientGender || ['gender', 'patientGender', 'xingbie', '性别'];
  420. for (const name of possibleNames) {
  421. // 查找匹配的 radio 元素
  422. const radio = document.querySelector(`input[type="radio"][name="${name}"][value="${value}"]`);
  423. if (radio) {
  424. radio.checked = true;
  425. radio.dispatchEvent(new Event('change', { bubbles: true }));
  426. radio.dispatchEvent(new Event('click', { bubbles: true }));
  427. // 高亮显示
  428. const formItem = radio.closest('.form-item');
  429. if (formItem) {
  430. highlightElement(formItem);
  431. }
  432. return true;
  433. }
  434. }
  435. console.log(`[医疗语音助手] 未找到性别字段,值: ${value}`);
  436. return false;
  437. }
  438. /**
  439. * 填写症状字段(多选框)
  440. */
  441. function fillSymptomsField(symptoms) {
  442. let filledCount = 0;
  443. // 先清空所有症状复选框
  444. const allCheckboxes = document.querySelectorAll('input[type="checkbox"][name="symptoms"], input[type="checkbox"][name="symptom"]');
  445. allCheckboxes.forEach(cb => {
  446. cb.checked = false;
  447. });
  448. // 勾选匹配的症状
  449. symptoms.forEach(symptom => {
  450. const checkbox = document.querySelector(`input[type="checkbox"][name="symptoms"][value="${symptom}"], input[type="checkbox"][name="symptom"][value="${symptom}"]`);
  451. if (checkbox) {
  452. checkbox.checked = true;
  453. checkbox.dispatchEvent(new Event('change', { bubbles: true }));
  454. checkbox.dispatchEvent(new Event('click', { bubbles: true }));
  455. filledCount++;
  456. }
  457. });
  458. // 如果至少填写了一个症状,高亮整个症状区域
  459. if (filledCount > 0) {
  460. const symptomContainer = document.querySelector('input[type="checkbox"][name="symptoms"]');
  461. if (symptomContainer) {
  462. const formItem = symptomContainer.closest('.form-item');
  463. if (formItem) {
  464. highlightElement(formItem);
  465. }
  466. }
  467. return true;
  468. }
  469. console.log(`[医疗语音助手] 未找到症状字段,值: [${symptoms.join(', ')}]`);
  470. return false;
  471. }
  472. /**
  473. * 查找表单输入框(支持多种匹配方式)
  474. */
  475. function findInput(possibleNames) {
  476. // 方式1: 通过ID匹配
  477. for (const name of possibleNames) {
  478. const element = document.getElementById(name);
  479. if (element) return element;
  480. }
  481. // 方式2: 通过name属性匹配
  482. const inputs = document.querySelectorAll('input, textarea, select');
  483. for (const input of inputs) {
  484. if (input.name && possibleNames.includes(input.name)) {
  485. return input;
  486. }
  487. }
  488. // 方式3: 通过placeholder匹配
  489. for (const input of inputs) {
  490. if (input.placeholder) {
  491. for (const name of possibleNames) {
  492. if (input.placeholder.includes(name)) {
  493. return input;
  494. }
  495. }
  496. }
  497. }
  498. // 方式4: 通过label文本匹配
  499. const labels = document.querySelectorAll('label');
  500. for (const label of labels) {
  501. for (const name of possibleNames) {
  502. if (label.textContent.includes(name)) {
  503. const forId = label.getAttribute('for');
  504. if (forId) {
  505. const input = document.getElementById(forId);
  506. if (input) return input;
  507. }
  508. }
  509. }
  510. }
  511. // 方式5: 使用XPath定位(最后的手段)
  512. for (const name of possibleNames) {
  513. const xpath = `//label[contains(text(), '${name}')]/../input | //label[contains(text(), '${name}')]/../textarea | //label[contains(text(), '${name}')]/../select`;
  514. const result = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
  515. if (result.singleNodeValue) {
  516. return result.singleNodeValue;
  517. }
  518. }
  519. console.log(`[医疗语音助手] 未找到字段: ${possibleNames[0]}`);
  520. return null;
  521. }
  522. /**
  523. * 高亮显示填写的字段
  524. */
  525. function highlightField(element) {
  526. element.style.backgroundColor = '#fff3cd';
  527. element.style.border = '2px solid #ffc107';
  528. setTimeout(() => {
  529. element.style.backgroundColor = '';
  530. element.style.border = '';
  531. }, 3000);
  532. }
  533. /**
  534. * 高亮显示元素(用于 form-item 等容器元素)
  535. */
  536. function highlightElement(element) {
  537. const originalBg = element.style.backgroundColor;
  538. const originalTransition = element.style.transition;
  539. element.style.transition = 'background-color 0.3s ease';
  540. element.style.backgroundColor = '#fff3cd';
  541. setTimeout(() => {
  542. element.style.backgroundColor = originalBg;
  543. setTimeout(() => {
  544. element.style.transition = originalTransition;
  545. }, 300);
  546. }, 1500);
  547. }
  548. /**
  549. * 显示提示消息
  550. */
  551. function showMessage(message) {
  552. let messageBox = document.getElementById('medical-voice-message');
  553. if (!messageBox) {
  554. messageBox = document.createElement('div');
  555. messageBox.id = 'medical-voice-message';
  556. messageBox.style.cssText = `
  557. position: fixed;
  558. top: 20px;
  559. right: 20px;
  560. background: #333;
  561. color: white;
  562. padding: 15px 25px;
  563. border-radius: 5px;
  564. z-index: 10001;
  565. font-size: 14px;
  566. box-shadow: 0 2px 10px rgba(0,0,0,0.2);
  567. `;
  568. document.body.appendChild(messageBox);
  569. }
  570. messageBox.textContent = message;
  571. messageBox.style.display = 'block';
  572. }
  573. /**
  574. * 隐藏提示消息
  575. */
  576. function hideMessage() {
  577. const messageBox = document.getElementById('medical-voice-message');
  578. if (messageBox) {
  579. messageBox.style.display = 'none';
  580. }
  581. }
  582. // 页面加载时初始化
  583. init();