fix(web_order_box): 修复移动端音频和语音播放限制问题

1. 调整响应式布局的最小高度阈值从420px改为300px
2. 添加unlockAudio和unlockSpeech函数,在用户手势上下文解锁浏览器自动播放限制
3. 优化播放逻辑,当音频上下文未解锁时跳过蜂鸣音播放
This commit is contained in:
yangxiangyuan
2026-07-30 16:31:38 +08:00
parent 1bf6d71201
commit 88ad9a5f06
2 changed files with 30 additions and 2 deletions
+1 -1
View File
@@ -406,7 +406,7 @@ input[type='number'] { -moz-appearance: textfield; appearance: textfield; }
margin-top: 0; 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-container { padding: 18px 28px; padding-top: calc(12px + env(safe-area-inset-top)); }
.runner-header { margin-bottom: 10px; } .runner-header { margin-bottom: 10px; }
.runner-progress { margin-bottom: 12px; } .runner-progress { margin-bottom: 12px; }
+29 -1
View File
@@ -627,6 +627,9 @@
updateStaleWarning(); updateStaleWarning();
return; return;
} }
// 必须在 await 之前(用户手势上下文中)解锁音频和语音
unlockAudio();
unlockSpeech();
setButtonLoading(btn, '启动中…'); setButtonLoading(btn, '启动中…');
const res = await runTrackedInteraction('启动执行会话', function () { const res = await runTrackedInteraction('启动执行会话', function () {
return apiFetch('POST', '/api/app_order_box/kits/motion_reminder/sessions/start', { card: card }); return apiFetch('POST', '/api/app_order_box/kits/motion_reminder/sessions/start', { card: card });
@@ -1061,6 +1064,7 @@
} }
function openRunner(session, card) { function openRunner(session, card) {
unlockAudio(); // 用户手势中解锁音频,确保后续定时器触发的提示音能播放
const steps = (card && Array.isArray(card.steps)) ? card.steps : []; const steps = (card && Array.isArray(card.steps)) ? card.steps : [];
if (!steps.length) { if (!steps.length) {
showAlert('无法启动', '当前卡片没有可执行步骤'); showAlert('无法启动', '当前卡片没有可执行步骤');
@@ -1355,6 +1359,17 @@
} catch (e) { /* 不支持时静默 */ } } 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() { function stopSpeech() {
try { try {
if ('speechSynthesis' in window) window.speechSynthesis.cancel(); if ('speechSynthesis' in window) window.speechSynthesis.cancel();
@@ -1384,12 +1399,25 @@
let audioCtx = null; 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() { function playBeep() {
try { try {
const AudioContextCtor = window.AudioContext || window.webkitAudioContext; const AudioContextCtor = window.AudioContext || window.webkitAudioContext;
if (!AudioContextCtor) return; if (!AudioContextCtor) return;
if (!audioCtx) audioCtx = new AudioContextCtor(); if (!audioCtx) audioCtx = new AudioContextCtor();
if (audioCtx.state === 'suspended') { void audioCtx.resume(); } // 如果仍处于 suspended(无用户手势解锁),跳过本次播放
if (audioCtx.state === 'suspended') return;
const osc = audioCtx.createOscillator(); const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain(); const gain = audioCtx.createGain();
osc.type = 'sine'; osc.type = 'sine';