diff --git a/public/tools/web_order_box/order_box.css b/public/tools/web_order_box/order_box.css index 8731d90..97c5c3d 100644 --- a/public/tools/web_order_box/order_box.css +++ b/public/tools/web_order_box/order_box.css @@ -406,7 +406,7 @@ input[type='number'] { -moz-appearance: textfield; appearance: textfield; } margin-top: 0; } -@media (orientation: landscape) and (min-height: 420px), (min-width: 900px) { +@media (orientation: landscape) and (min-height: 300px), (min-width: 900px) { .runner-container { padding: 18px 28px; padding-top: calc(12px + env(safe-area-inset-top)); } .runner-header { margin-bottom: 10px; } .runner-progress { margin-bottom: 12px; } diff --git a/public/tools/web_order_box/order_box.js b/public/tools/web_order_box/order_box.js index 9c7085a..36c94ef 100644 --- a/public/tools/web_order_box/order_box.js +++ b/public/tools/web_order_box/order_box.js @@ -627,6 +627,9 @@ updateStaleWarning(); return; } + // 必须在 await 之前(用户手势上下文中)解锁音频和语音 + unlockAudio(); + unlockSpeech(); setButtonLoading(btn, '启动中…'); const res = await runTrackedInteraction('启动执行会话', function () { return apiFetch('POST', '/api/app_order_box/kits/motion_reminder/sessions/start', { card: card }); @@ -1061,6 +1064,7 @@ } function openRunner(session, card) { + unlockAudio(); // 用户手势中解锁音频,确保后续定时器触发的提示音能播放 const steps = (card && Array.isArray(card.steps)) ? card.steps : []; if (!steps.length) { showAlert('无法启动', '当前卡片没有可执行步骤'); @@ -1355,6 +1359,17 @@ } catch (e) { /* 不支持时静默 */ } } + /** 在用户手势中调用,解锁 iOS Safari 的语音合成限制 */ + function unlockSpeech() { + try { + if (!('speechSynthesis' in window)) return; + const u = new SpeechSynthesisUtterance(''); + u.volume = 0; + window.speechSynthesis.speak(u); + window.speechSynthesis.cancel(); + } catch (e) { /* 静默 */ } + } + function stopSpeech() { try { if ('speechSynthesis' in window) window.speechSynthesis.cancel(); @@ -1384,12 +1399,25 @@ let audioCtx = null; + /** 在用户手势中调用,解锁 AudioContext(解决浏览器自动播放策略静音问题) */ + function unlockAudio() { + try { + const AudioContextCtor = window.AudioContext || window.webkitAudioContext; + if (!AudioContextCtor) return; + if (!audioCtx) audioCtx = new AudioContextCtor(); + if (audioCtx.state === 'suspended') { + audioCtx.resume().catch(function () {}); + } + } catch (e) { /* 不支持时静默 */ } + } + function playBeep() { try { const AudioContextCtor = window.AudioContext || window.webkitAudioContext; if (!AudioContextCtor) return; if (!audioCtx) audioCtx = new AudioContextCtor(); - if (audioCtx.state === 'suspended') { void audioCtx.resume(); } + // 如果仍处于 suspended(无用户手势解锁),跳过本次播放 + if (audioCtx.state === 'suspended') return; const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); osc.type = 'sine';