
白噪音页面包含雨声、海浪和篝火三个场景。场景变化不仅替换标题还会改变符号、渐变背景、按钮颜色和提示。如果正在“播放雨声”时直接切到海浪却保留playing true页面会立即显示海浪正在播放但实际播放器并没有完成音源切换。当前版本因此在选择场景时主动复位播放状态。interfaceSoundItem{id:numbername:stringsubtitle:stringsymbol:stringcolorA:stringcolorB:string}StatecurrentIndex:number0Stateplaying:booleanfalseStatevolume:number55StatetimerMinutes:number30当前场景由索引读取切换时同步清理播放状态并更新提示播放按钮只负责开始与暂停。privatecurrent():SoundItem{returnthis.sounds[this.currentIndex]}privatechoose(index:number):void{this.currentIndexindexthis.playingfalse// 切换场景后等待重新开始this.message已切换到${this.current().name}}privatetoggle():void{this.playing!this.playingthis.messagethis.playing?${this.current().name}正在播放:已经暂停}顶部视觉区域全部读取current()名称、说明、符号和渐变颜色会在一次索引更新后同步变化。音量与定时选项则独立保存切换场景不会丢失用户设定。.linearGradient({angle:165,colors:[[this.current().colorA,0.0],[this.current().colorB,1.0]]})Slider({value:this.volume,min:0,max:100}).selectedColor(this.current().colorA).onChange((value:number){this.volumevalue})这里把“场景相关状态”和“全局控制状态”分开主题色、标题和符号随场景变化音量和定时设置在场景切换时保留。若把音量也放进SoundItem每个场景可以有独立音量但当前需求并不需要这种复杂度。定时关闭按钮直接修改timerMinutes并根据当前值切换颜色。它目前只是设置项没有真正启动倒计时这一点从代码也能看出来ForEach([15,30,60],(minutes:number){Button(${minutes}).fontSize(12).fontColor(this.timerMinutesminutes?Color.White:#606B7E).backgroundColor(this.timerMinutesminutes?this.current().colorA:#EEF1F5).onClick((){this.timerMinutesminutes})})播放按钮同样只切换布尔状态与文案。页面表现已经形成闭环但音频能力仍是下一层实现不能把UI状态当作播放器回调状态。privateasyncprepareSound(index:number):Promisevoid{constversion:numberthis.requestVersionthis.phasepreparingawaitthis.releasePlayer()constplayer:media.AVPlayerawaitmedia.createAVPlayer()if(version!this.requestVersion){awaitplayer.release()return}this.playerplayerthis.bindPlayerEvents(player,version)player.urlthis.sounds[index].source}音频资源释放也要进入组件生命周期。切换场景、页面退出和播放失败都可能持有播放器对象必须定义统一清理路径否则多次进入页面后可能残留音频或占用资源。当前纯状态版本没有创建播放器对象因此没有伪造这部分代码。场景切换与播放开关是两类事件场景列表只维护当前索引播放按钮只维护播放状态。切换音源时主动回到暂停避免页面已经显示“海浪”播放器却还在继续上一段雨声privatecurrent():SoundItem{returnthis.sounds[this.currentIndex]}privatechoose(index:number):void{this.currentIndexindexthis.playingfalsethis.message已切换到${this.current().title}}privatetoggle():void{this.playing!this.playingthis.messagethis.playing?正在播放:已暂停}索引适用于当前固定数组如果允许用户下载、删除和排序音源当前项应改存稳定 ID再通过查找得到对象。否则删除当前项之前的一条记录会使索引指向另一个场景。privateapplyVolume(value:number):void{this.volumeMath.max(0,Math.min(100,value))constgain:numberthis.volume/100this.player?.setVolume(gain)}privateasynctogglePlayer():Promisevoid{if(!this.player||!this.canToggle())returnif(this.phaseplaying){awaitthis.player.pause()}else{awaitthis.player.play()}}调用播放接口并不等于已经发声准备、缓冲、播放和失败需要独立状态按钮文案应监听状态回调而不是在点击瞬间直接假定成功。接入音频后的状态机实际实现可以把页面状态划分为 idle、preparing、ready、playing、paused、error。选择场景后进入 preparing资源准备完成后进入 readyprivatebindPlayerEvents(player:media.AVPlayer,version:number):void{player.on(stateChange,(state:string){if(version!this.requestVersion)returnif(stateprepared)this.phasereadyif(stateplaying)this.phaseplayingif(statepaused)this.phasepausedif(stateerror)this.phaseerror})}用户修改为“不开启”时取消任务切换音源时保留还是重置定时器则要形成一致规则。真正到时后调用暂停并更新播放器状态不能只把按钮文字改成“已停止”。页面生命周期与异常恢复组件销毁时清理计时器和播放器监听音频是否继续后台播放则由产品能力决定。若不支持后台播放页面离开即暂停并释放若支持就把播放器迁移到后台服务页面只订阅服务状态。privateasyncreleasePlayer():Promisevoid{if(!this.player)returnthis.player.off(stateChange)awaitthis.player.release()this.playerundefinedthis.phaseidle}aboutToDisappear():void{this.cancelSleepTimer()this.requestVersionthis.releasePlayer()}当前原型只验证页面交互因此没有用虚假的计时器伪装真实播放这让后续接入系统媒体能力时边界更清楚。可重复的交互验证原型运行时依次验证四条链路选择海浪后当前卡片和标题同步且播放状态复位连续点击播放与暂停时文案不漂移拖动音量只改变数值不意外切换音源修改定时选项后只保留一个当前值。privatearmSleepTimer(minutes:number):void{this.cancelSleepTimer()this.timerMinutesminutesthis.sleepEndAtDate.now()minutes*60*1000this.sleepTimerIdsetInterval((){if(Date.now()this.sleepEndAt)returnthis.player?.pause()this.phasepausedthis.cancelSleepTimer()},1000)}privatecancelSleepTimer():void{if(this.sleepTimerId0)clearInterval(this.sleepTimerId)this.sleepTimerId-1}音量、音源和播放阶段还可以记录为调试快照但日志只保留枚举和数值不输出本地资源路径或用户数据。复现异常时按选择事件、准备回调、播放命令、状态回调的顺序查看能够判断问题发生在页面分发还是媒体层。播放按钮由阶段决定是否可用播放器状态不再压缩成一个布尔值typePlayerPhaseidle|preparing|ready|playing|paused|errorprivatecanToggle():boolean{return[ready,playing,paused].includes(this.phase)}privateplayButtonText():string{if(this.phasepreparing)return准备中if(this.phaseplaying)return暂停if(this.phaseerror)return重新加载return播放}按钮禁用、加载反馈和错误重试都直接映射播放器阶段页面不会提前显示“正在播放”。