因公司业务需求,要求我在小程序中增加一个播放器,ui照着网易云画的,去github屎里掏金了半天都是旧版的仿制,所以自己整了个,当然主要的功能实现还是多亏了claude4,省了我很多事。
接下来就是代码实现(不涉及公司信息)
<template>
<view class="flex-col page">
<view class="flex-col justify-start section">
<view class="flex-col justify-start section_2">
<view class="flex-row relative section_3">
<image class="shrink-0 image_6 pos_7"
src="https://tmf.bimhui.com/yuanxinapp/434598bf55fad60f3a1bb2fa03846917.png" />
<image class="shrink-0 image_5 pos_6"
src="https://tmf.bimhui.com/yuanxinapp/45720b5d243d18bfcd1b24de14186985.png" />
<text class="text_2 pos_10">音频播放</text>
<image class="shrink-0 image_7 pos_9"
src="https://tmf.bimhui.com/yuanxinapp/1d2be95baecf8712ddfd3705a3213a0a.png" @click="goToMusicDetail" />
<image class="shrink-0 image_10 pos_11"
src="https://tmf.bimhui.com/yuanxinapp/f2483d8969f6ba88a6a81db169489c3c.png" />
<image class="shrink-0 image pos"
src="https://tmf.bimhui.com/yuanxinapp/a2474e70c7d79152aa9fb4955594e4b7.png" />
</view>
</view>
</view>
<view class="mt-12 flex-col relative group">
<view class="self-stretch group_2"></view>
<text class="self-start text_3">{{ musicInfo.title }}</text>
<text class="self-start font text_4">{{ musicInfo.artist }}</text>
<view class="flex-col justify-start self-stretch relative group_3">
<view class="flex-col justify-start items-start section_7" @touchstart="onProgressTouchStart"
@touchmove="onProgressTouchMove" @touchend="onProgressTouchEnd">
<view class="shrink-0 section_8" :style="{ width: progressWidth + 'rpx' }"></view>
</view>
<view class="section_6"
:style="{ position: 'absolute', left: progressPosition + 'rpx', top: '-2rpx', zIndex: 10 }"
@touchstart="onSliderTouchStart" @touchmove="onSliderTouchMove" @touchend="onSliderTouchEnd"></view>
</view>
<view class="flex-row justify-between self-stretch group_4">
<text class="font">{{ formatTime(currentTime) }}</text>
<text class="font">{{ formatTime(duration) }}</text>
</view>
<view class="flex-row justify-between items-center self-stretch group_5">
<image class="image_13" src="https://tmf.bimhui.com/yuanxinapp/4fe54501edc493932d686ef598cedac6.png"
@click="toggleLoop" />
<image class="image_13" src="https://tmf.bimhui.com/yuanxinapp/1f4a2fef79417999a0fff63545714403.png"
@click="previousSong" />
<view class="flex-col justify-start items-center image-wrapper_2" @click="togglePlay">
<image class="image_12" :src="isPlaying ? pauseIcon : playIcon" />
</view>
<image class="image_13" src="https://tmf.bimhui.com/yuanxinapp/c0241c91e565d66db79d2e1beca7d2f0.png"
@click="nextSong" />
<image class="image_13" src="https://tmf.bimhui.com/yuanxinapp/3115d36bc1043fb8b471788ce5abeca7.png"
@click="toggleMute" />
</view>
<image class="image_11 pos_12" :src="musicInfo.cover" />
</view>
</view>
</template>
<script>
export default {
components: {},
props: {},
data() {
return {
// 歌曲列表
playlist: [
{
title: '希望有羽毛和翅膀',
artist: '知更鸟,HOYO-MiX,Chevy',
cover: 'https://tmf.bimhui.com/yuanxinapp/e1865e1e52e5d214d0d02aa358075881.png',
src: 'https://tmf.bimhui.com/yuanxinapp/知更鸟,HOYO-MiX,Chevy - 希望有羽毛和翅膀.mp3',
duration: 0 // 预估时长设为0,防止无法判断音频是否加载成功
},
{
title: '不眠之夜',
artist: '张杰,HOYO-MiX',
cover: 'https://tmf.bimhui.com/yuanxinapp/e1865e1e52e5d214d0d02aa358075881.png',
src: 'https://tmf.bimhui.com/yuanxinapp/张杰,HOYO-MiX - 不眠之夜.mp3',
duration: 0 // 预估时长设为0,防止无法判断音频是否加载成功
},
{
title: '在银河中孤独摇摆',
artist: '知更鸟,HOYO-MiX,Chevy',
cover: 'https://tmf.bimhui.com/yuanxinapp/e1865e1e52e5d214d0d02aa358075881.png', // 暂时使用相同封面
src: 'https://tmf.bimhui.com/yuanxinapp/知更鸟,HOYO-MiX,Chevy - 在银河中孤独摇摆.mp3',
duration: 0 // 预估时长设为0,防止无法判断音频是否加载成功
},
{
title: '耀斑',
artist: 'HOYO-MiX,YMIR',
cover: 'https://tmf.bimhui.com/yuanxinapp/e1865e1e52e5d214d0d02aa358075881.png', // 暂时使用相同封面
src: 'https://tmf.bimhui.com/yuanxinapp/HOYO-MiX,YMIR - 耀斑.mp3',
duration: 0 // 预估时长设为0,防止无法判断音频是否加载成功
},
{
title: '拂晓 Proi Proi',
artist: 'HOYO-MiX,NIDA',
cover: 'https://tmf.bimhui.com/yuanxinapp/e1865e1e52e5d214d0d02aa358075881.png', // 暂时使用相同封面
src: 'https://tmf.bimhui.com/yuanxinapp/HOYO-MiX,NIDA - 拂晓 Proi Proi.mp3',
duration: 0 // 预估时长设为0,防止无法判断音频是否加载成功
},
],
// 当前播放的歌曲索引
currentSongIndex: 0,
// 播放状态
isPlaying: false,
currentTime: 0,
duration: 0, // 当前歌曲的时长,初始为0,实际播放时会自动获取
isLoop: false,
isMuted: false,
// 音频状态管理
audioState: 'idle', // idle, loading, ready, playing, paused, error
isAudioOperationInProgress: false, // 防止并发操作
audioLoadPromise: null, // 音频加载Promise
// 进度条相关
progressBarWidth: 686, // 进度条总宽度(rpx)
isDragging: false,
dragStartTime: 0, // 拖拽开始时的时间
lastDragTime: 0, // 上次拖拽的时间,用于防抖
// 测试用的模拟播放
testTimer: null,
seekTimer: null, // 防抖跳转定时器
// 音频管理
audioContext: null,
backgroundAudioManager: null,
// 图标
playIcon: 'https://tmf.bimhui.com/yuanxinapp/c941904fbcb7613763d50b98889c9f0a.png',
pauseIcon: 'https://tmf.bimhui.com/yuanxinapp/pause.png', // 暂时使用相同图标
// URL管理
originalUrl: '', // 存储原始URL
encodedUrl: '', // 存储编码后的URL
// 环境检测
isRealDevice: false // 是否为真机环境
};
},
computed: {
// 计算进度条宽度
progressWidth() {
if (this.duration === 0) return 0;
return (this.currentTime / this.duration) * this.progressBarWidth;
},
// 计算滑块位置
progressPosition() {
if (this.duration === 0) return -17; // 初始位置在左边缘
// 计算进度百分比,然后转换为实际像素位置
const progress = this.currentTime / this.duration;
const maxPosition = this.progressBarWidth - 34; // 减去滑块宽度
return Math.max(-17, Math.min(progress * this.progressBarWidth - 17, maxPosition - 17));
},
// 获取当前播放的歌曲信息
musicInfo() {
return this.playlist[this.currentSongIndex];
}
},
mounted() {
const systemInfo = uni.getSystemInfoSync();
// 检查小程序基础库版本
if (systemInfo.SDKVersion) {
const sdkVersion = systemInfo.SDKVersion;
console.log('小程序基础库版本:', sdkVersion);
// 背景音频需要基础库 1.2.0 以上
const minVersion = '1.2.0';
if (this.compareVersion(sdkVersion, minVersion) < 0) {
console.warn(`当前基础库版本 ${sdkVersion} 过低,背景音频需要 ${minVersion} 以上版本`);
uni.showModal({
title: '版本提示',
content: '当前微信版本过低,可能无法正常播放背景音频,建议升级到最新版本',
showCancel: false
});
}
}
// 验证播放列表数据完整性
if (!this.playlist || this.playlist.length === 0) {
return;
}
if (!this.musicInfo) {
return;
}
if (!this.musicInfo.src) {
return;
}
// 初始化第一首歌曲的时长
this.updateCurrentDuration();
// 处理初始歌曲的URL编码
this.processCurrentSongUrl();
// 设置初始音频状态
this.audioState = 'loading';
this.initAudio();
// 初始化完成后设置为就绪状态
setTimeout(() => {
if (this.audioState === 'loading') {
this.audioState = 'ready';
// 检查音频管理器是否成功初始化
if (!this.backgroundAudioManager && !this.audioContext) {
uni.showModal({
title: '初始化失败',
content: '音频播放器初始化失败,可能是由于权限限制或设备兼容性问题。将使用演示模式。',
showCancel: false,
confirmText: '知道了',
success: () => {
this.showAudioError();
}
});
}
}
}, 500);
// 检测真机环境
this.detectRealDevice();
// 延迟启动测试模式,确保音频初始化完成
setTimeout(() => {
this.startTestMode();
}, 500);
},
methods: {
// 初始化音频
initAudio() {
try {
// 小程序使用背景音频管理器
this.backgroundAudioManager = uni.getBackgroundAudioManager();
if (!this.backgroundAudioManager) {
throw new Error('背景音频管理器初始化失败');
}
this.setupBackgroundAudioEvents();
// 如果背景音频管理器不可用,尝试使用InnerAudioContext作为备用方案
if (!this.backgroundAudioManager) {
try {
this.audioContext = uni.createInnerAudioContext();
if (this.audioContext) {
this.setupAudioContextEvents();
}
} catch (innerAudioError) {
// 静默处理错误
}
}
// 智能处理音频URL
if (this.musicInfo.src) {
// 检查URL是否有效
if (this.musicInfo.src.startsWith('http://') || this.musicInfo.src.startsWith('https://')) {
// 检查URL是否包含需要编码的字符(中文、逗号、空格等)
const needsEncoding = /[\u4e00-\u9fa5,\s]/.test(this.musicInfo.src);
if (needsEncoding) {
try {
// 分离URL的基础部分和文件名部分
const urlParts = this.musicInfo.src.split('/');
const protocol = urlParts[0]; // https:
const domain = urlParts[2]; // tmf.bimhui.com
const pathParts = urlParts.slice(3); // ['yuanxinapp', '知更鸟,HOYO-MiX,Chevy - 希望有羽毛和翅膀.mp3']
// 对路径的每个部分进行编码
const encodedPathParts = pathParts.map(part => encodeURIComponent(part));
// 重新组装URL
const encodedUrl = `${protocol}//${domain}/${encodedPathParts.join('/')}`;
// 存储两个版本的URL
this.originalUrl = this.musicInfo.src;
this.encodedUrl = encodedUrl;
// 直接使用编码后的URL替换原始URL
this.musicInfo.src = encodedUrl;
} catch (encodeError) {
// 静默处理编码错误
}
}
}
}
} catch (error) {
uni.showToast({
title: '音频初始化失败',
icon: 'none'
});
}
},
// 设置背景音频管理器事件(小程序)
setupBackgroundAudioEvents() {
if (!this.backgroundAudioManager) return;
// 添加安全访问背景音频管理器属性的方法
const safeGetProperty = (property, defaultValue = 0) => {
try {
return this.backgroundAudioManager[property] || defaultValue;
} catch (error) {
return defaultValue;
}
};
this.backgroundAudioManager.onCanplay(() => {
try {
const duration = safeGetProperty('duration');
this.updateAudioDuration('onCanplay', duration);
} catch (error) {
// 静默处理
}
});
// 添加更多事件来确保获取准确时长
this.backgroundAudioManager.onLoadedMetadata && this.backgroundAudioManager.onLoadedMetadata(() => {
try {
const duration = safeGetProperty('duration');
this.updateAudioDuration('onLoadedMetadata', duration);
} catch (error) {
// 静默处理
}
});
this.backgroundAudioManager.onTimeUpdate(() => {
try {
if (!this.isDragging) {
this.currentTime = safeGetProperty('currentTime', 0);
// 在播放过程中也检查时长是否有更新
const duration = safeGetProperty('duration');
if (duration && duration !== this.duration) {
this.updateAudioDuration('onTimeUpdate', duration);
}
// 检查并校正时长
this.checkAndCorrectDuration();
}
} catch (error) {
// 静默处理
}
});
this.backgroundAudioManager.onPlay(() => {
this.isPlaying = true;
this.audioState = 'playing';
});
this.backgroundAudioManager.onPause(() => {
this.isPlaying = false;
this.audioState = 'paused';
});
this.backgroundAudioManager.onEnded(() => {
this.onAudioEnded();
});
this.backgroundAudioManager.onError((error) => {
// 安全获取背景音频管理器的状态信息
let audioManagerInfo = {};
try {
audioManagerInfo = {
src: this.backgroundAudioManager.src || 'undefined',
title: this.backgroundAudioManager.title || 'undefined',
paused: this.backgroundAudioManager.paused,
currentTime: this.backgroundAudioManager.currentTime || 0,
duration: this.backgroundAudioManager.duration || 0
};
} catch (stateError) {
audioManagerInfo = { error: '无法获取状态信息' };
}
this.isPlaying = false;
this.audioState = 'error';
this.isAudioOperationInProgress = false; // 重置操作锁
// 根据错误码和错误信息提供更具体的错误信息
let errorMessage = '音频播放失败';
// 检查是否是 getTingAudioState 相关错误
if (error.errMsg && error.errMsg.includes('getTingAudioState')) {
errorMessage = '音频状态获取失败,可能是设备兼容性问题';
} else if (error.errCode === 10001) {
errorMessage = '系统错误';
} else if (error.errCode === 10002) {
errorMessage = '网络错误,请检查网络连接';
} else if (error.errCode === 10003) {
errorMessage = '文件错误,音频文件可能损坏';
} else if (error.errCode === 10004) {
errorMessage = '格式错误,不支持的音频格式';
} else if (audioManagerInfo.src === 'undefined' || audioManagerInfo.src === 'null') {
errorMessage = '音频源地址为空,请检查音频文件路径';
}
uni.showToast({
title: errorMessage,
icon: 'none',
duration: 3000
});
// 如果是状态获取错误或音频源问题,尝试使用备用方案
if ((error.errMsg && error.errMsg.includes('getTingAudioState')) ||
audioManagerInfo.src === 'undefined' ||
audioManagerInfo.src === 'null') {
setTimeout(() => {
this.handleBackgroundAudioFallback();
}, 1000);
}
});
this.backgroundAudioManager.onWaiting(() => {
// 音频缓冲中
});
},
// 设置音频上下文事件(H5和App)
setupAudioContextEvents() {
if (!this.audioContext) return;
this.audioContext.onTimeUpdate(() => {
if (!this.isDragging) {
this.currentTime = this.audioContext.currentTime || 0;
// 在播放过程中也检查时长是否有更新
if (this.audioContext.duration && this.audioContext.duration !== this.duration) {
this.updateAudioDuration('audioContext-onTimeUpdate', this.audioContext.duration);
}
// 检查并校正时长
this.checkAndCorrectDuration();
}
});
this.audioContext.onCanplay(() => {
this.updateAudioDuration('audioContext-onCanplay', this.audioContext.duration);
});
// 添加loadedmetadata事件监听
this.audioContext.onLoadedMetadata && this.audioContext.onLoadedMetadata(() => {
this.updateAudioDuration('audioContext-onLoadedMetadata', this.audioContext.duration);
});
this.audioContext.onPlay(() => {
this.isPlaying = true;
this.audioState = 'playing';
console.log('音频开始播放');
});
this.audioContext.onPause(() => {
this.isPlaying = false;
this.audioState = 'paused';
console.log('音频暂停');
});
this.audioContext.onEnded(() => {
console.log('音频播放结束');
this.onAudioEnded();
});
this.audioContext.onError((error) => {
console.error('音频播放错误:', error);
this.isPlaying = false;
this.audioState = 'error';
this.isAudioOperationInProgress = false; // 重置操作锁
uni.showToast({
title: '音频播放失败,请检查网络连接',
icon: 'none'
});
});
this.audioContext.onWaiting(() => {
console.log('音频缓冲中...');
});
},
// 验证音频源是否有效
validateAudioSource() {
if (!this.musicInfo) {
return false;
}
if (!this.musicInfo.src) {
return false;
}
if (typeof this.musicInfo.src !== 'string') {
return false;
}
if (this.musicInfo.src.trim() === '') {
return false;
}
if (!this.musicInfo.src.startsWith('http://') && !this.musicInfo.src.startsWith('https://')) {
return false;
}
return true;
},
// 测试音频URL是否可访问
testAudioUrl() {
return new Promise((resolve) => {
// 先验证音频源
if (!this.validateAudioSource()) {
resolve(false);
return;
}
uni.request({
url: this.musicInfo.src,
method: 'HEAD',
success: (res) => {
if (res.statusCode === 200) {
resolve(true);
} else {
resolve(false);
}
},
fail: (error) => {
resolve(false);
}
});
});
},
// 播放/暂停切换
async togglePlay() {
// 防止并发操作
if (this.isAudioOperationInProgress) {
return;
}
this.isAudioOperationInProgress = true;
try {
if (this.audioState === 'error') {
await this.reinitializeAudio();
}
// 如果还没播放过,先测试URL
if (this.audioState === 'idle' || (this.audioState === 'ready' && this.currentTime === 0)) {
await this.tryPlayAudio();
} else {
await this.doTogglePlay();
}
} catch (error) {
this.audioState = 'error';
uni.showToast({
title: '播放失败: ' + error.message,
icon: 'none'
});
// 如果真实播放失败,回退到测试模式
this.toggleTestPlay();
} finally {
this.isAudioOperationInProgress = false;
}
},
// 重新初始化音频
async reinitializeAudio() {
this.audioState = 'loading';
try {
// 停止当前音频
this.stopCurrentSong();
// 重新初始化
await new Promise(resolve => {
setTimeout(() => {
this.initAudio();
this.audioState = 'ready';
resolve();
}, 100);
});
} catch (error) {
this.audioState = 'error';
throw error;
}
},
// 尝试播放音频,如果失败则尝试其他URL
async tryPlayAudio() {
try {
this.audioState = 'loading';
console.log('开始测试音频URL可用性');
console.log('当前使用的URL:', this.musicInfo.src);
// 测试当前URL(已经在初始化时进行了编码处理)
const isValid = await this.testAudioUrl();
if (isValid) {
console.log('音频URL可用,开始播放');
this.audioState = 'ready';
await this.doTogglePlay();
} else {
console.log('音频URL测试失败');
// 如果当前URL失败,且存在原始URL,尝试原始URL
if (this.originalUrl && this.originalUrl !== this.musicInfo.src) {
console.log('尝试使用原始URL:', this.originalUrl);
const currentSrc = this.musicInfo.src;
this.musicInfo.src = this.originalUrl;
const originalValid = await this.testAudioUrl();
if (originalValid) {
console.log('原始URL可用,开始播放');
this.audioState = 'ready';
await this.doTogglePlay();
} else {
// 恢复编码URL并显示错误
this.musicInfo.src = currentSrc;
console.log('所有URL都无法访问');
this.audioState = 'error';
this.showAudioError();
}
} else {
console.log('没有备用URL可尝试');
this.audioState = 'error';
this.showAudioError();
}
}
} catch (error) {
console.error('尝试播放音频失败:', error);
this.audioState = 'error';
throw error;
}
},
// 处理背景音频降级
handleBackgroundAudioFallback() {
// 先尝试使用 InnerAudioContext
if (!this.audioContext) {
try {
this.audioContext = uni.createInnerAudioContext();
if (this.audioContext) {
this.setupAudioContextEvents();
uni.showModal({
title: '切换播放方式',
content: '背景音频不兼容,已切换到普通音频播放模式。\n\n注意:切换到其他应用时音频会暂停。',
showCancel: false,
confirmText: '知道了'
});
return;
}
} catch (error) {
// 静默处理
}
}
// 如果 InnerAudioContext 也不可用,显示错误信息
this.showAudioError();
},
// 显示音频错误提示
showAudioError() {
uni.showModal({
title: '音频播放器提示',
content: '真机环境下音频播放可能受到以下因素影响:\n\n1. 小程序音频播放权限\n2. 网络连接状态\n3. 音频文件访问权限\n4. 设备兼容性问题\n5. 微信版本或基础库版本\n6. getTingAudioState API兼容性\n\n现在将使用演示模式展示播放器功能(进度条会动但无声音)',
showCancel: false,
confirmText: '知道了',
success: () => {
this.toggleTestPlay();
}
});
},
// 执行实际的播放/暂停操作
async doTogglePlay() {
try {
// 小程序优先使用背景音频管理器
if (this.backgroundAudioManager) {
console.log('使用背景音频管理器播放');
if (this.isPlaying) {
console.log('=== 执行暂停操作 ===');
console.log('当前音频状态:', {
src: this.backgroundAudioManager.src,
paused: this.backgroundAudioManager.paused,
currentTime: this.backgroundAudioManager.currentTime
});
await this.safeAudioOperation(() => {
this.backgroundAudioManager.pause();
});
this.audioState = 'paused';
console.log('暂停操作完成');
} else {
console.log('=== 执行播放操作 ===');
// 验证音频源
if (!this.validateAudioSource()) {
console.error('音频源验证失败,无法播放');
throw new Error('音频源无效');
}
// 检查是否是续播(已有音频源且处于暂停状态)
const isResume = this.backgroundAudioManager.src &&
this.backgroundAudioManager.src === this.musicInfo.src &&
this.audioState === 'paused';
if (isResume) {
console.log('检测到续播操作');
// 使用专门的真机续播处理方法
const resumeSuccess = await this.handleRealDeviceResume();
if (!resumeSuccess) {
console.log('真机续播失败,尝试标准续播方法');
await this.safeAudioOperation(() => {
this.backgroundAudioManager.play();
});
this.audioState = 'playing';
}
console.log('续播操作完成');
} else {
console.log('检测到新播放操作,设置音频信息');
console.log('设置背景音频信息:', {
title: this.musicInfo.title,
singer: this.musicInfo.artist,
cover: this.musicInfo.cover,
src: this.musicInfo.src
});
// 安全设置背景音频管理器属性
try {
console.log('开始设置背景音频管理器属性');
// 逐个安全设置属性
const setProperty = (property, value, description) => {
try {
this.backgroundAudioManager[property] = value;
console.log(`${description}设置成功:`, value);
} catch (error) {
console.warn(`${description}设置失败:`, error);
// 继续执行,不抛出错误
}
};
setProperty('title', this.musicInfo.title, '音频标题');
setProperty('singer', this.musicInfo.artist, '音频演唱者');
setProperty('coverImgUrl', this.musicInfo.cover, '音频封面');
// 延迟设置src,确保其他属性先设置完成
setTimeout(() => {
try {
this.backgroundAudioManager.src = this.musicInfo.src;
} catch (srcError) {
// 如果src设置失败,尝试降级方案
this.handleBackgroundAudioFallback();
}
}, 150);
} catch (managerError) {
console.error('设置背景音频管理器失败:', managerError);
// 不直接抛出错误,而是尝试降级方案
this.handleBackgroundAudioFallback();
return;
}
// 设置src后会自动播放
this.audioState = 'playing';
}
}
return;
}
// 如果背景音频管理器不可用,使用InnerAudioContext
if (this.audioContext) {
console.log('背景音频管理器不可用,使用InnerAudioContext');
if (this.isPlaying) {
console.log('暂停播放');
await this.safeAudioOperation(() => {
this.audioContext.pause();
});
this.audioState = 'paused';
} else {
console.log('开始播放');
// 验证音频源
if (!this.validateAudioSource()) {
console.error('音频源验证失败,无法播放');
throw new Error('音频源无效');
}
this.audioContext.src = this.musicInfo.src;
console.log('InnerAudioContext音频源设置完成');
await this.safeAudioOperation(() => {
this.audioContext.play();
});
this.audioState = 'playing';
}
return;
}
// 如果没有音频管理器,使用测试模式
console.log('使用测试播放模式');
this.toggleTestPlay();
this.audioState = this.isPlaying ? 'playing' : 'paused';
} catch (error) {
console.error('实际播放操作失败:', error);
this.audioState = 'error';
this.toggleTestPlay();
}
},
// 真机环境下的续播处理
async handleRealDeviceResume() {
console.log('=== 真机续播处理 ===');
if (!this.backgroundAudioManager) {
console.warn('背景音频管理器不存在,无法续播');
return false;
}
try {
// 检查背景音频管理器状态
const audioState = {
src: this.backgroundAudioManager.src,
paused: this.backgroundAudioManager.paused,
currentTime: this.backgroundAudioManager.currentTime,
duration: this.backgroundAudioManager.duration
};
console.log('当前背景音频状态:', audioState);
// 如果音频处于暂停状态且有有效的src,尝试续播
if (audioState.src && audioState.src === this.musicInfo.src) {
console.log('执行续播操作');
// 在真机环境下,有时需要强制重新设置一些属性
this.backgroundAudioManager.title = this.musicInfo.title;
// 调用play方法
this.backgroundAudioManager.play();
// 等待播放状态更新
return new Promise((resolve) => {
let checkCount = 0;
const maxChecks = 10;
const checkPlayState = () => {
checkCount++;
console.log(`续播状态检查 ${checkCount}/${maxChecks}:`, {
isPlaying: this.isPlaying,
paused: this.backgroundAudioManager.paused
});
if (this.isPlaying || checkCount >= maxChecks) {
if (this.isPlaying) {
console.log('续播成功');
resolve(true);
} else {
console.warn('续播可能失败,但不阻断流程');
resolve(false);
}
} else {
setTimeout(checkPlayState, 200);
}
};
setTimeout(checkPlayState, 100);
});
} else {
console.log('不满足续播条件,需要重新设置音频源');
return false;
}
} catch (error) {
console.error('真机续播处理失败:', error);
return false;
}
},
// 安全的音频操作包装器
async safeAudioOperation(operation) {
return new Promise((resolve, reject) => {
try {
const result = operation();
// 如果operation返回Promise,等待它完成
if (result && typeof result.then === 'function') {
result
.then(() => resolve())
.catch((error) => {
console.warn('音频操作Promise被拒绝:', error);
// 即使Promise被拒绝,也不完全失败,可能是快速切换导致的
resolve();
});
} else {
// 给操作一些时间完成
setTimeout(() => resolve(), 50);
}
} catch (error) {
console.error('音频操作执行失败:', error);
reject(error);
}
});
},
// 音频播放结束
onAudioEnded() {
console.log('音频播放结束,循环模式:', this.isLoop);
if (this.isLoop) {
// 循环播放当前歌曲
console.log('循环播放:重新开始当前歌曲');
this.currentTime = 0;
// 确保播放状态正确,为重新播放做准备
this.isPlaying = false;
// 清理可能存在的定时器
if (this.testTimer) {
clearInterval(this.testTimer);
this.testTimer = null;
}
// 重新开始播放
setTimeout(() => {
console.log('循环播放:开始播放');
this.togglePlay();
}, 200);
} else {
// 检查是否还有下一首歌曲
if (this.currentSongIndex < this.playlist.length - 1) {
console.log('当前歌曲播放完毕,自动播放下一首');
// 自动播放下一首
this.nextSong();
} else {
console.log('播放列表已结束');
this.isPlaying = false;
this.currentTime = 0;
uni.showToast({
title: '播放列表已结束',
icon: 'none'
});
}
}
},
// 设置播放时间
setCurrentTime(time) {
// 这个方法现在由 seekToPosition 替代,保留是为了向后兼容
this.seekToPosition(time);
},
// 更新当前歌曲的时长
updateCurrentDuration() {
this.duration = this.musicInfo.duration;
console.log('初始化歌曲时长:', this.formatTime(this.duration));
console.log('注意:实际播放时长可能与预设不同,系统会自动校正');
},
// 更新音频时长
updateAudioDuration(eventType, duration) {
if (duration !== undefined && duration !== null && duration > 0) {
const oldDuration = this.duration;
this.duration = duration;
if (Math.abs(oldDuration - duration) > 1) { // 时长差异超过1秒才输出日志
console.log(`音频时长更新 (事件: ${eventType}): ${this.formatTime(oldDuration)} → ${this.formatTime(this.duration)}`);
}
// 更新playlist中的时长信息
this.playlist[this.currentSongIndex].duration = duration;
} else {
console.warn(`音频时长未从事件 ${eventType} 获取到有效值,当前时长:`, this.formatTime(this.duration));
}
},
// 检查并校正时长
checkAndCorrectDuration() {
// 如果当前播放时间超过了预设时长,说明时长不准确
if (this.currentTime > this.duration && this.currentTime > 0) {
const newDuration = Math.ceil(this.currentTime + 10); // 增加10秒缓冲
console.log(`时长校正: 播放时间 ${this.formatTime(this.currentTime)} 超过预设时长 ${this.formatTime(this.duration)}`);
console.log(`自动校正时长为: ${this.formatTime(newDuration)}`);
this.updateAudioDuration('auto-correction', newDuration);
}
},
// 进度条触摸开始
onProgressTouchStart(e) {
this.isDragging = true;
this.dragStartTime = this.currentTime;
this.updateProgress(e);
// 添加视觉反馈
this.showProgressFeedback(true);
// 防止页面滚动
e.preventDefault && e.preventDefault();
},
// 进度条触摸移动
onProgressTouchMove(e) {
if (this.isDragging) {
this.updateProgress(e);
// 防止页面滚动
e.preventDefault && e.preventDefault();
}
},
// 进度条触摸结束
onProgressTouchEnd(e) {
console.log('进度跳转到:', this.formatTime(this.currentTime));
this.isDragging = false;
this.showProgressFeedback(false);
// 执行音乐跳转(添加防抖)
this.debouncedSeek(this.currentTime);
},
// 滑块触摸开始
onSliderTouchStart(e) {
this.isDragging = true;
this.dragStartTime = this.currentTime;
this.showProgressFeedback(true);
// 防止页面滚动
e.preventDefault && e.preventDefault();
},
// 滑块触摸移动
onSliderTouchMove(e) {
if (this.isDragging) {
this.updateProgress(e);
// 防止页面滚动
e.preventDefault && e.preventDefault();
}
},
// 滑块触摸结束
onSliderTouchEnd(e) {
console.log('进度跳转到:', this.formatTime(this.currentTime));
this.isDragging = false;
this.showProgressFeedback(false);
// 执行音乐跳转(添加防抖)
this.debouncedSeek(this.currentTime);
},
// 更新进度
updateProgress(e) {
const touch = e.touches[0];
uni.createSelectorQuery().in(this).select('.section_7').boundingClientRect((rect) => {
if (rect) {
let offsetX = touch.clientX - rect.left;
offsetX = Math.max(0, Math.min(offsetX, rect.width));
const progress = offsetX / rect.width;
const newTime = progress * this.duration;
// 限制在有效范围内
this.currentTime = Math.max(0, Math.min(newTime, this.duration));
}
}).exec();
},
// 防抖跳转
debouncedSeek(time) {
// 清除之前的定时器
if (this.seekTimer) {
clearTimeout(this.seekTimer);
}
// 设置新的定时器
this.seekTimer = setTimeout(() => {
this.seekToPosition(time);
this.seekTimer = null;
}, 200); // 200ms 防抖
},
// 跳转到指定位置
seekToPosition(time) {
try {
console.log('执行音乐跳转到:', this.formatTime(time));
if (this.backgroundAudioManager) {
try {
// 安全检查背景音频管理器状态
const src = this.backgroundAudioManager.src;
if (src && src !== 'undefined' && src !== 'null') {
// 小程序背景音频跳转
this.backgroundAudioManager.seek(time);
uni.showToast({
title: `跳转到 ${this.formatTime(time)}`,
icon: 'none',
duration: 1000
});
return;
} else {
console.warn('背景音频src为空,无法执行跳转');
}
} catch (seekError) {
console.warn('背景音频跳转失败:', seekError);
// 继续尝试其他方案
}
}
if (this.audioContext && this.audioContext.src) {
// InnerAudioContext音频跳转
this.audioContext.seek(time);
uni.showToast({
title: `跳转到 ${this.formatTime(time)}`,
icon: 'none',
duration: 1000
});
return;
}
// 如果是测试模式,直接设置时间
console.log('测试模式下的时间跳转');
uni.showToast({
title: `演示模式:跳转到 ${this.formatTime(time)}`,
icon: 'none',
duration: 1000
});
// 在测试模式下,如果正在播放,重新同步定时器
if (this.isPlaying && this.testTimer) {
// 重新开始计时
clearInterval(this.testTimer);
this.testTimer = setInterval(() => {
if (!this.isDragging) {
this.currentTime += 1;
if (this.currentTime >= this.duration) {
if (this.isLoop) {
this.currentTime = 0;
} else {
this.currentTime = this.duration;
clearInterval(this.testTimer);
this.testTimer = null;
this.isPlaying = false;
}
}
}
}, 1000);
}
} catch (error) {
console.error('音乐跳转失败:', error);
uni.showToast({
title: '跳转失败',
icon: 'none'
});
}
},
// 显示进度反馈
showProgressFeedback(show) {
// 可以在这里添加拖拽时的视觉效果
// 可以添加一些视觉反馈,比如改变滑块颜色等
// 这里可以设置一些状态来改变UI样式
},
// 格式化时间
formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
},
// 切换循环模式
toggleLoop() {
this.isLoop = !this.isLoop;
uni.showToast({
title: this.isLoop ? '已开启循环播放' : '已关闭循环播放',
icon: 'none'
});
},
// 切换静音
toggleMute() {
try {
// #ifdef H5 || APP-PLUS
if (this.audioContext) {
// InnerAudioContext 没有直接的静音方法,这里通过暂停实现
if (this.isMuted) {
this.audioContext.play();
} else {
this.audioContext.pause();
}
}
// #endif
this.isMuted = !this.isMuted;
uni.showToast({
title: this.isMuted ? '已静音' : '已取消静音',
icon: 'none'
});
} catch (error) {
console.error('静音操作失败:', error);
}
},
// 上一首
previousSong() {
console.log('切换到上一首歌曲');
// 保存当前播放状态
const wasPlaying = this.isPlaying;
console.log('保存播放状态:', wasPlaying);
// 停止当前播放并重置状态
this.stopCurrentSong();
this.resetPlaybackState();
// 切换到上一首
this.currentSongIndex = this.currentSongIndex === 0
? this.playlist.length - 1
: this.currentSongIndex - 1;
// 更新歌曲时长
this.updateCurrentDuration();
console.log('当前歌曲:', this.musicInfo.title);
// 显示切换提示
uni.showToast({
title: `切换到:${this.musicInfo.title}`,
icon: 'none',
duration: 2000
});
// 重新初始化音频并恢复播放状态
setTimeout(() => {
// 先处理新歌曲的URL编码
this.processCurrentSongUrl();
this.initAudio();
// 如果之前在播放,自动播放新歌曲
if (wasPlaying) {
console.log('恢复播放状态,开始播放新歌曲');
setTimeout(() => {
this.togglePlay();
}, 500);
}
}, 300);
},
// 下一首
nextSong() {
console.log('切换到下一首歌曲');
// 保存当前播放状态
const wasPlaying = this.isPlaying;
console.log('保存播放状态:', wasPlaying);
// 停止当前播放并重置状态
this.stopCurrentSong();
this.resetPlaybackState();
// 切换到下一首
this.currentSongIndex = (this.currentSongIndex + 1) % this.playlist.length;
// 更新歌曲时长
this.updateCurrentDuration();
console.log('当前歌曲:', this.musicInfo.title);
// 显示切换提示
uni.showToast({
title: `切换到:${this.musicInfo.title}`,
icon: 'none',
duration: 2000
});
// 重新初始化音频并恢复播放状态
setTimeout(() => {
// 先处理新歌曲的URL编码
this.processCurrentSongUrl();
this.initAudio();
// 如果之前在播放,自动播放新歌曲
if (wasPlaying) {
console.log('恢复播放状态,开始播放新歌曲');
setTimeout(() => {
this.togglePlay();
}, 500);
}
}, 300);
},
// 返回上一页
goToMusicDetail() {
// 停止播放
this.stopAudio();
uni.navigateBack({
delta: 1
});
},
// 停止音频播放
stopAudio() {
try {
if (this.backgroundAudioManager) {
this.backgroundAudioManager.stop();
}
if (this.audioContext) {
this.audioContext.stop();
}
this.isPlaying = false;
// 清除测试定时器
if (this.testTimer) {
clearInterval(this.testTimer);
this.testTimer = null;
}
} catch (error) {
// 静默处理
}
},
// 停止当前播放的歌曲
stopCurrentSong() {
// 清理定时器
if (this.testTimer) {
clearInterval(this.testTimer);
this.testTimer = null;
}
if (this.seekTimer) {
clearTimeout(this.seekTimer);
this.seekTimer = null;
}
// 清理音频源
try {
if (this.backgroundAudioManager) {
this.backgroundAudioManager.stop();
}
if (this.audioContext) {
this.audioContext.stop();
}
} catch (error) {
// 静默处理
}
},
// 重置播放状态
resetPlaybackState() {
this.isPlaying = false;
this.currentTime = 0;
this.isDragging = false;
// 清理定时器
if (this.testTimer) {
clearInterval(this.testTimer);
this.testTimer = null;
}
if (this.seekTimer) {
clearTimeout(this.seekTimer);
this.seekTimer = null;
}
},
// 检测真机环境
detectRealDevice() {
const systemInfo = uni.getSystemInfoSync();
const isRealDevice = systemInfo.platform !== 'devtools';
if (isRealDevice) {
// 存储真机环境标识
this.isRealDevice = true;
} else {
this.isRealDevice = false;
}
},
// 显示URL编码测试结果
// 启动测试模式(用于验证滑块跟随)
startTestMode() {
// 音频播放器已就绪
},
// 处理当前歌曲的URL编码
processCurrentSongUrl() {
if (!this.musicInfo || !this.musicInfo.src) {
return;
}
const originalSrc = this.musicInfo.src;
// 检查URL是否包含需要编码的字符(中文、逗号、空格等)
const needsEncoding = /[\u4e00-\u9fa5,\s]/.test(originalSrc);
if (needsEncoding && originalSrc.startsWith('http')) {
try {
// 分离URL的基础部分和文件名部分
const urlParts = originalSrc.split('/');
const protocol = urlParts[0]; // https:
const domain = urlParts[2]; // tmf.bimhui.com
const pathParts = urlParts.slice(3); // ['yuanxinapp', '知更鸟,HOYO-MiX,Chevy - 希望有羽毛和翅膀.mp3']
// 对路径的每个部分进行编码
const encodedPathParts = pathParts.map(part => encodeURIComponent(part));
// 重新组装URL
const encodedUrl = `${protocol}//${domain}/${encodedPathParts.join('/')}`;
// 存储两个版本的URL
this.originalUrl = originalSrc;
this.encodedUrl = encodedUrl;
// 直接使用编码后的URL替换原始URL
this.musicInfo.src = encodedUrl;
} catch (encodeError) {
// 静默处理编码错误
}
} else {
// 清空之前的编码缓存
this.originalUrl = '';
this.encodedUrl = '';
}
},
// 版本比较函数
compareVersion(version1, version2) {
const v1 = version1.split('.');
const v2 = version2.split('.');
const len = Math.max(v1.length, v2.length);
while (v1.length < len) {
v1.push('0');
}
while (v2.length < len) {
v2.push('0');
}
for (let i = 0; i < len; i++) {
const num1 = parseInt(v1[i]);
const num2 = parseInt(v2[i]);
if (num1 > num2) {
return 1;
} else if (num1 < num2) {
return -1;
}
}
return 0;
},
// 切换测试播放
toggleTestPlay() {
if (this.isPlaying) {
// 停止测试
if (this.testTimer) {
clearInterval(this.testTimer);
this.testTimer = null;
}
this.isPlaying = false;
} else {
// 开始测试播放
this.isPlaying = true;
this.testTimer = setInterval(() => {
if (!this.isDragging) {
this.currentTime += 1;
// 在测试模式中也检查并校正时长
this.checkAndCorrectDuration();
if (this.currentTime >= this.duration) {
if (this.isLoop) {
this.currentTime = 0;
} else {
this.currentTime = this.duration;
clearInterval(this.testTimer);
this.testTimer = null;
this.isPlaying = false;
}
}
}
}, 1000);
}
}
},
// 页面卸载时停止播放
beforeDestroy() {
this.stopAudio();
// 清理测试定时器
if (this.testTimer) {
clearInterval(this.testTimer);
this.testTimer = null;
}
// 清理防抖定时器
if (this.seekTimer) {
clearTimeout(this.seekTimer);
this.seekTimer = null;
}
try {
if (this.audioContext) {
this.audioContext.destroy();
}
} catch (error) {
// 静默处理
}
}
};
</script>
<style scoped lang="scss">
.ml-11 {
margin-left: 22rpx;
}
.page {
padding-bottom: 166rpx;
background-color: #1d1311;
width: 100%;
overflow-y: auto;
overflow-x: hidden;
height: 100%;
.section {
background-color: #1d1311;
overflow: hidden;
height: 176.4rpx;
.section_2 {
overflow: hidden;
background-color: #ffffff00;
height: 184rpx;
border-left: solid 2rpx #707070;
border-right: solid 2rpx #707070;
border-top: solid 2rpx #707070;
border-bottom: solid 2rpx #707070;
.section_3 {
padding: 26rpx 12rpx 64rpx 34rpx;
background-color: #ffffff00;
overflow: hidden;
height: 230rpx;
.image_6 {
width: 36rpx;
height: 32rpx;
}
.pos_7 {
position: absolute;
right: 210rpx;
top: 50%;
transform: translateY(-50%);
}
.image_4 {
width: 48rpx;
height: 22.66rpx;
}
.pos_5 {
position: absolute;
right: 30rpx;
top: 36rpx;
}
.image_3 {
width: 30rpx;
height: 22rpx;
}
.pos_4 {
position: absolute;
right: 88rpx;
top: 36rpx;
}
.image_2 {
width: 34rpx;
height: 22rpx;
}
.pos_3 {
position: absolute;
right: 128rpx;
top: 36rpx;
}
.text-wrapper {
padding: 8rpx 0;
overflow: hidden;
width: 64rpx;
.text {
color: #ffffff;
font-size: 30rpx;
line-height: 22.26rpx;
}
}
.pos_2 {
position: absolute;
left: 62rpx;
top: 26rpx;
}
.image_5 {
width: 46rpx;
height: 12rpx;
}
.pos_6 {
position: absolute;
left: 78rpx;
top: 86rpx;
}
.section_4 {
padding: 12rpx 24rpx;
background-color: #4c4c4c80;
border-radius: 50rpx;
width: 174rpx;
border-left: solid 1rpx #96969633;
border-right: solid 1rpx #96969633;
border-top: solid 1rpx #96969633;
border-bottom: solid 1rpx #96969633;
.image-wrapper {
width: 38rpx;
.image_9 {
width: 38rpx;
height: 14rpx;
}
.image_8 {
width: 34rpx;
height: 34rpx;
}
}
.section_5 {
background-color: #00000033;
width: 1rpx;
height: 37rpx;
}
}
.pos_8 {
position: absolute;
right: 12rpx;
top: 100rpx;
}
.text_2 {
color: #ffffff;
font-size: 34rpx;
font-family: Alibaba PuHuiTi;
line-height: 31.62rpx;
opacity: 0.9;
}
.pos_10 {
position: absolute;
right: 300rpx;
bottom: 82.38rpx;
}
.image_7 {
width: 18rpx;
height: 34rpx;
}
.pos_9 {
position: absolute;
left: 34rpx;
bottom: 80rpx;
}
.image_10 {
width: 4rpx;
height: 4rpx;
}
.pos_11 {
position: absolute;
left: 124rpx;
bottom: 64rpx;
}
.image {
width: 97.6vw;
height: 23.4667vw;
}
.pos {
position: absolute;
left: 18rpx;
right: 0;
top: 0;
}
}
}
}
.group {
padding: 0 32rpx;
.group_2 {
border-radius: 56rpx;
overflow: hidden;
height: 972rpx;
}
.text_3 {
margin-top: 28rpx;
color: #ffffff;
font-size: 40rpx;
font-family: PingFang SC;
line-height: 37.36rpx;
}
.text_4 {
margin-top: 24rpx;
line-height: 25.9rpx;
}
.group_3 {
margin-top: 32rpx;
padding: 12rpx 0;
.section_7 {
background-color: #363d46;
border-radius: 40rpx;
overflow: hidden;
width: 686rpx;
height: 12rpx;
position: relative;
.section_8 {
background-color: #7bcdc3;
border-radius: 40rpx;
overflow: hidden;
width: 184rpx;
height: 12rpx;
}
}
.section_6 {
background-color: #ffffff;
border-radius: 50%;
width: 34rpx;
height: 34rpx;
}
}
.group_4 {
margin-top: 36rpx;
}
.group_5 {
margin-top: 32rpx;
.image_13 {
width: 56rpx;
height: 56rpx;
}
.image-wrapper_2 {
padding: 20rpx 0;
background-color: #49515a6b;
border-radius: 50%;
width: 104rpx;
height: 104rpx;
.image_12 {
width: 64rpx;
height: 64rpx;
}
}
}
.image_11 {
border-radius: 56rpx;
overflow: hidden;
width: 91.2vw;
height: 129.6vw;
}
.pos_12 {
position: absolute;
left: 34rpx;
right: 32rpx;
top: 0;
}
}
.font {
font-size: 28rpx;
font-family: PingFang SC;
line-height: 20.78rpx;
color: #a9b0c2;
}
}
</style>