diff --git a/registry/lib/components/video/audio-only-mode/AudioOnlyMode.vue b/registry/lib/components/video/audio-only-mode/AudioOnlyMode.vue index 139e9469bf..5cebf43d96 100644 --- a/registry/lib/components/video/audio-only-mode/AudioOnlyMode.vue +++ b/registry/lib/components/video/audio-only-mode/AudioOnlyMode.vue @@ -15,6 +15,7 @@ import { Toast } from '@/core/toast' import { Options } from './index' const console = useScopedConsole('听视频') +const BLUR_AUTO_ENABLE_DELAY_MS = 30 * 1000 export default Vue.extend({ components: { @@ -26,6 +27,8 @@ export default Vue.extend({ isAudioMode: false, disabled: false, settings, + blurTimer: null as ReturnType | null, + initialAutoEnableAttempted: false as boolean, } }, computed: { @@ -37,14 +40,19 @@ export default Vue.extend({ }, }, async mounted() { - videoChange(() => { + videoChange(async () => { this.isAudioMode = false - if (this.settings.options.autoEnable) { + if (this.settings.options.autoEnable && !this.initialAutoEnableAttempted) { + this.initialAutoEnableAttempted = true + // Wait briefly for the player to settle after a no-refresh video change + // before attempting to switch to audio mode, to avoid AbortError. + await new Promise(r => setTimeout(r, 800)) this.switchToAudioMode() } }) - if (this.settings.options.autoEnable) { + if (this.settings.options.autoEnable && !this.initialAutoEnableAttempted) { + this.initialAutoEnableAttempted = true await this.switchToAudioMode() } @@ -53,8 +61,52 @@ export default Vue.extend({ this.switchToAudioMode() } }) + + if (this.settings.options.autoEnableOnBlur) { + this.setupBlurListener() + } + + addComponentListener('audioOnlyMode.autoEnableOnBlur', (value: boolean) => { + if (value) { + this.setupBlurListener() + } else { + this.teardownBlurListener() + } + }) + }, + beforeDestroy() { + this.teardownBlurListener() }, methods: { + setupBlurListener() { + // Remove first to prevent duplicate registrations if called multiple times. + document.removeEventListener('visibilitychange', this.handleVisibilityChange) + document.addEventListener('visibilitychange', this.handleVisibilityChange) + }, + teardownBlurListener() { + document.removeEventListener('visibilitychange', this.handleVisibilityChange) + this.clearBlurTimer() + }, + handleVisibilityChange() { + if (document.hidden) { + // Clear any existing timer before starting a new one to avoid orphaned timeouts. + this.clearBlurTimer() + this.blurTimer = setTimeout(() => { + // Re-check visibility to avoid switching modes if the page is visible again. + if (document.hidden && !this.isAudioMode) { + this.switchToAudioMode() + } + }, BLUR_AUTO_ENABLE_DELAY_MS) + } else { + this.clearBlurTimer() + } + }, + clearBlurTimer() { + if (this.blurTimer !== null) { + clearTimeout(this.blurTimer) + this.blurTimer = null + } + }, async toggleAudioMode() { if (this.isAudioMode) { Toast.info('请刷新页面以退出音频模式', '听视频', 2000) @@ -179,10 +231,24 @@ export default Vue.extend({ video.src = audioUrl video.load() - await video.play().catch((err: Error) => { + + let playAborted = false + await video.play().catch((err: DOMException) => { + if (err.name === 'AbortError') { + // AbortError is expected when a no-refresh video navigation interrupts + // the play() call before it can complete. This is not a real failure; + // the next videoChange event will trigger another switch attempt. + console.warn('播放被中止 (AbortError),可能由视频切换引起,将等待下次触发') + playAborted = true + return + } throw new Error(`播放失败: ${err.message}`) }) + if (playAborted) { + return + } + this.isAudioMode = true Toast.success('已切换到音频模式', '听视频', 2000) console.log('已切换到音频模式') diff --git a/registry/lib/components/video/audio-only-mode/index.ts b/registry/lib/components/video/audio-only-mode/index.ts index fa4fb65d6d..3cd28f889a 100644 --- a/registry/lib/components/video/audio-only-mode/index.ts +++ b/registry/lib/components/video/audio-only-mode/index.ts @@ -11,6 +11,10 @@ export const options = defineOptionsMetadata({ defaultValue: false, displayName: '自动启用', }, + autoEnableOnBlur: { + defaultValue: false, + displayName: '失去焦点后自动启用', + }, rememberProgress: { defaultValue: true, displayName: '记住播放进度',