<view class="listen">
<view class="header">
<text class="title">Listening</text>
<view class="right">
<text class="book-icon">?</text>
<text class="link">绘本馆</text>
</view>
</view>
<image :src="cover" class="cover"></image>
<text class="book">{{ title }}</text>
<view class="slider-wrap">
<slider
:value="progress"
:disabled="totalSeconds===0"
@changing="onChanging"
@change="onSeek"
activeColor="#0E4B3B"
backgroundColor="#e0e0e0"
block-size="20"
step="1"
min="0"
max="100"
/>
<view class="time-row">
<text class="t">{{ displayCurrentLabel }}</text>
<view class="rate-pill" @tap="changeRate">{{ rateLabel }}</view>
<text class="t">{{ displayDurationLabel }}</text>
</view>
</view>
<view class="controls">
<text class="ctrl" @tap="toggleShuffle">?</text>
<text class="ctrl" @tap="prev15">⏮️</text>
<view class="play" @tap="togglePlay">
<view v-if="showSpinner" class="loading-spinner"></view>
<text v-else class="play-icon">{{ playing ? '⏸️' : '▶️' }}</text>
</view>
<text class="ctrl" @tap="next15">⏭️</text>
<text class="ctrl" @tap="toggleLoop">?</text>
</view>
</view>
</template>
<script>
export default {
data(){
return {
cover: '/static/images/sample-cover.jpg',
title: 'Sample Story',
src: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3',
playing: false,
loading: false,
progress: 0,
currentLabel: '00:00',
durationLabel: '03:45',
totalSeconds: 225,
currentSeconds: 0,
loop: false,
rates: [0.75, 1, 1.5],
rateIdx: 1,
timer: null,
audioManager: null,
useRealAudio: true,
isHarmony: false,
supportRate: true,
isDragging: false,
previewSeconds: 0
}
},
computed:{
rate(){ return this.rates[this.rateIdx] },
rateLabel(){ return this.rate + 'x' },
displayCurrentLabel(){
if(this.isDragging) {
return this.formatTime(this.previewSeconds) + ' ?'
}
if(this.loading) {
return this.currentLabel + ' ⏳'
}
return this.currentLabel
},
displayDurationLabel(){
return this.formatTime(this.totalSeconds)
},
showSpinner() {
return this.loading
}
},
onLoad(query){
if(query && query.title){
this.title = decodeURIComponent(query.title)
}
console.log('熏听页面加载,标题:', this.title)
// 延迟初始化音频,避免启动时崩溃
setTimeout(() => {
this.initAudio()
}, 500)
},
onUnload() {
this.cleanup()
},
methods: {
initAudio() {
try {
// 检测平台
const sys = uni.getSystemInfoSync && uni.getSystemInfoSync()
if((sys && /harmony/i.test(sys.osName||'')) || (typeof plus!=='undefined' && plus.os && /harmony/i.test(plus.os.name||''))){
this.isHarmony = true
this.supportRate = false
}
// 尝试创建背景音频管理器
this.audioManager = uni.getBackgroundAudioManager()
this.audioManager.title = this.title
this.audioManager.src = this.src
// 设置加载状态
this.loading = true
// 绑定音频事件
this.audioManager.onWaiting(() => {
console.log('音频缓冲中,设置loading状态')
this.loading = true
// loading状态会由onCanplay或onPlay事件清除
})
this.audioManager.onCanplay(() => {
console.log('音频可以播放')
this.loading = false
})
this.audioManager.onPlay(() => {
console.log('音频开始播放')
this.playing = true
this.loading = false
this.startProgressTimer()
})
this.audioManager.onPause(() => {
console.log('音频暂停')
this.playing = false
this.loading = false
this.clearTimer()
})
this.audioManager.onStop(() => {
console.log('音频停止')
this.playing = false
this.loading = false
this.progress = 0
this.currentSeconds = 0
this.updateDisplay()
this.clearTimer()
})
this.audioManager.onEnded(() => {
console.log('音频播放结束')
this.loading = false
if(this.loop) {
this.loading = true
this.audioManager.seek(0)
this.audioManager.play()
} else {
this.playing = false
this.clearTimer()
}
})
this.audioManager.onTimeUpdate(() => {
if(this.audioManager.duration > 0) {
this.totalSeconds = this.audioManager.duration || 0
this.currentSeconds = this.audioManager.currentTime || 0
this.updateDisplay()
}
})
this.audioManager.onError((e) => {
console.warn('音频播放出错:', e)
this.loading = false
this.fallbackToSimulation()
})
console.log('音频管理器初始化成功')
} catch(e) {
console.warn('音频管理器初始化失败,使用模拟播放:', e)
this.loading = false
this.fallbackToSimulation()
}
},
fallbackToSimulation() {
console.log('切换到模拟播放模式')
this.useRealAudio = false
this.audioManager = null
},
formatTime(seconds) {
if(!seconds || seconds < 0) return '00:00'
const mins = Math.floor(seconds / 60) || 0
const secs = Math.floor(seconds % 60) || 0
return `${mins.toString().padStart(2,'0')}:${secs.toString().padStart(2,'0')}`
},
updateDisplay() {
this.currentLabel = this.formatTime(this.currentSeconds || 0)
this.durationLabel = this.formatTime(this.totalSeconds || 0)
// 拖拽时不更新progress,避免冲突
if(!this.isDragging) {
if(this.totalSeconds > 0) {
this.progress = Math.floor((this.currentSeconds / this.totalSeconds) * 100) || 0
} else {
this.progress = 0
}
}
},
startProgressTimer() {
this.clearTimer()
if(!this.useRealAudio) {
this.timer = setInterval(() => {
if(this.playing && this.currentSeconds < this.totalSeconds) {
this.currentSeconds += this.rate
this.updateDisplay()
} else if(this.currentSeconds >= this.totalSeconds) {
if(this.loop) {
this.currentSeconds = 0
this.updateDisplay()
} else {
this.playing = false
this.clearTimer()
}
}
}, 1000)
}
},
togglePlay(){
if(this.loading) {
uni.showToast({
title: '音频加载中,请稍候',
icon: 'none'
})
return
}
if(this.useRealAudio && this.audioManager) {
try {
if(this.playing) {
this.audioManager.pause()
} else {
this.loading = true
this.audioManager.play()
}
} catch(e) {
console.warn('音频操作失败,切换到模拟模式:', e)
this.loading = false
this.fallbackToSimulation()
this.togglePlaySimulation()
}
} else {
this.togglePlaySimulation()
}
},
togglePlaySimulation() {
this.playing = !this.playing
uni.showToast({
title: this.playing ? '开始播放(模拟)' : '暂停播放',
icon: 'none'
})
if(this.playing) {
this.startProgressTimer()
} else {
this.clearTimer()
}
},
clearTimer() {
if(this.timer) {
clearInterval(this.timer)
this.timer = null
}
},
onChanging(e) {
console.log('滑块拖拽中:', e.detail.value)
this.isDragging = true
const value = Number(e.detail.value) || 0
// 计算预览时间
if(this.totalSeconds > 0) {
this.previewSeconds = Math.max(0, (this.totalSeconds * value) / 100)
} else {
this.previewSeconds = 0
}
// 注意:不需要手动设置progress,slider组件会自动处理
console.log('预览时间:', this.previewSeconds, '秒')
},
onSeek(e) {
console.log('滑块拖拽结束:', e.detail.value)
const value = Number(e.detail.value) || 0
if(this.totalSeconds <= 0) {
this.isDragging = false
return
}
const targetSeconds = Math.max(0, (this.totalSeconds * value) / 100)
console.log('跳转到目标时间:', targetSeconds, '秒')
// 立即更新currentSeconds,确保界面同步
this.currentSeconds = targetSeconds
if(this.useRealAudio && this.audioManager) {
try {
console.log('真实音频跳转')
this.loading = true
this.audioManager.seek(targetSeconds)
} catch(e) {
console.warn('音频跳转失败:', e)
}
} else {
console.log('模拟音频跳转')
// 模拟音频直接更新
}
// 更新显示并清除拖拽状态
this.updateDisplay()
this.isDragging = false
},
prev15(){
const newTime = Math.max(0, (this.currentSeconds || 0) - 15)
if(this.useRealAudio && this.audioManager) {
try {
this.audioManager.seek(newTime)
} catch(e) {
console.warn('跳转失败:', e)
this.currentSeconds = newTime
this.updateDisplay()
}
} else {
this.currentSeconds = newTime
this.updateDisplay()
}
uni.showToast({ title: '后退15秒', icon: 'none' })
},
next15(){
const newTime = Math.min((this.totalSeconds || 0), (this.currentSeconds || 0) + 15)
if(this.useRealAudio && this.audioManager) {
try {
this.audioManager.seek(newTime)
} catch(e) {
console.warn('跳转失败:', e)
this.currentSeconds = newTime
this.updateDisplay()
}
} else {
this.currentSeconds = newTime
this.updateDisplay()
}
uni.showToast({ title: '快进15秒', icon: 'none' })
},
changeRate(){
if(!this.supportRate) {
uni.showToast({ title:'当前平台不支持倍速', icon:'none' })
return
}
this.rateIdx = (this.rateIdx + 1) % this.rates.length
if(this.useRealAudio && this.audioManager) {
try {
this.audioManager.playbackRate = this.rate
} catch(e) {
console.warn('设置倍速失败:', e)
this.supportRate = false
}
}
uni.showToast({ title: '倍速 ' + this.rate + 'x', icon:'none' })
},
toggleLoop(){
this.loop = !this.loop
uni.showToast({ title: this.loop ? '循环播放' : '关闭循环', icon:'none' })
},
toggleShuffle(){
uni.showToast({ title:'随机播放(占位)', icon:'none' })
},
cleanup() {
this.clearTimer()
if(this.audioManager) {
try {
this.audioManager.stop()
} catch(e) {
console.warn('停止音频失败:', e)
}
}
}
}
}
</script>
<style scoped>
.listen{
padding: 12rpx;
background: #f5f7f9;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: space-between;
box-sizing: border-box;
}
.header{
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6rpx;
flex-shrink: 0;
}
.title{
font-size: 40rpx;
font-weight: 800;
color: #333;
}
.right{
display: flex;
align-items: center;
color: #0E4B3B;
}
.book-icon{
margin-right: 6rpx;
}
.link{
color: #0E4B3B;
}
.cover{
width: 280rpx;
height: 320rpx;
border-radius: 8rpx;
display: block;
margin: 12rpx auto 6rpx;
flex-shrink: 0;
}
.book{
text-align: center;
font-size: 24rpx;
font-weight: 700;
color: #333;
margin-bottom: 12rpx;
flex-shrink: 0;
}
.slider-wrap{
padding: 0 16rpx;
margin: 6rpx 0;
flex-shrink: 0;
}
.time-row{
display: flex;
justify-content: space-between;
align-items: center;
color: #7a7f7d;
font-size: 20rpx;
margin-top: 4rpx;
}
.t{
min-width: 70rpx;
text-align: center;
}
.rate-pill{
padding: 4rpx 8rpx;
border-radius: 12rpx;
background: #e8f3ef;
color: #0E4B3B;
font-size: 18rpx;
}
.controls{
display: flex;
justify-content: space-around;
align-items: center;
margin: 12rpx 0;
flex-shrink: 0;
}
.ctrl{
font-size: 32rpx;
color: #0E4B3B;
}
.play{
width: 100rpx;
height: 100rpx;
border-radius: 50rpx;
background: #0E4B3B;
display: flex;
align-items: center;
justify-content: center;
}
.play-icon{
color: white;
font-size: 40rpx;
}
.loading-spinner {
width: 40rpx;
height: 40rpx;
border: 4rpx solid rgba(255, 255, 255, 0.3);
border-top: 4rpx solid white;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style> ```

- 发布:2025-09-28 13:33
- 更新:2025-09-28 13:33
- 阅读:23
产品分类: uniapp/App
PC开发环境操作系统: Windows
PC开发环境操作系统版本号: win11
HBuilderX类型: 正式
HBuilderX版本号: 4.76
手机系统: HarmonyOS NEXT
手机系统版本号: HarmonyOS 5.1.0
手机厂商: 模拟器
手机机型: 模拟器api19
页面类型: vue
vue版本: vue3
打包方式: 云端
项目创建方式: HBuilderX
示例代码:
操作步骤:
直接点播放音频,安卓上可以有loading效果,鸿蒙就没有
直接点播放音频,安卓上可以有loading效果,鸿蒙就没有
预期结果:
onWaiting执行,出现loading
onWaiting执行,出现loading
实际结果:
onWaiting并没有执行
onWaiting并没有执行
bug描述:
鸿蒙系统下使用getBackgroundAudioManager,onWaiting监听不会执行,onWaiting之后的onPlay也不会执行,onPlay只会执行第一次
0 个回复