uniapp实现ai聊天室
分类: 前端
简介: <template> <view class="flex col justify start relative page"> <! 顶部自定义导航 > <tn nav bar fixed alpha customBack> <view slot="back" class="tn custom nav bar__back" @click="goBack"> <text class="icon tn icon left"></text> <text class="icon tn icon home capsule fill"></text> </view> </tn nav bar> <! 对话容器 > <scroll view class="chat container" scroll y="true" :scroll into view="scrollToViewId" :style="{ paddingTop: vuex_custom_bar_height + 20 + 'px' }"> <! 消息列表 按顺序显示所有消息 > <view v for="(msg, index) in messageList" :key="index" class="message item"> <! 用户消息 > <view v if="msg.type === 'user'" class="user message container"> <view class="user message content"> <view class="user message bubble"> <text class="user message text">{{ msg.content }}</text> </view> </view> <image class="user avatar" src="https://tmf.bimhui.com/TimeFlow_Human_app/a07479c17fa5e2042bf9439d5cae65b5.png" /> </view> <! AI消息 > <view v else if="msg.type === 'ai'" class="ai message container"> <image class="ai avatar" src="https://tmf.bimhui.com/TimeFlow_Human_app/a07479c17fa5e2042bf9439d5cae65b5.png" /> <view class="ai message content"> <view class="ai message bubble"> <text class="ai message text">{{ msg.content }}</text> <! 操作按钮区域 > <view class="action buttons"> <view class="action item" @click="copyText(msg.content)"> <image class="action icon" src="https://tmf.bimhui.com/TimeFlow_Human_app/a76cddf3e49df3b5db52c1b72b5459b1.png" /> <text class="font_3">复制</text> </view> <view class="action item"> <image class="action icon" src="https://tmf.bimhui.com/TimeFlow_Human_app/d24162dc9a11013f39843b6383713a8d.png" /> <text class="font_3 text_5">去创作</text> </view> </view> </view> </view> </view> </view> <! AI正在输入提示 > <view class="ai typing indicator" v if="isAiTyping"> <image class="ai avatar" src="https://tmf.bimhui.com/TimeFlow_Human_app/a07479c17fa5e2042bf9439d5cae65b5.png" /> <view class="typing content"> <text class="typing text">AI正在思考中</text> <view class="typing dots"> <view class="dot"></view> <view class="dot"></view> <view class="dot"></view> </view> </view> </view> <! 滚动定位元素 > <view id="scroll bottom" class="scroll anchor"></view> </scroll view> <view class="flex row equal division"> <view class="flex row items center equal division item_1"> <image class="shrink 0 image_10" src="https://tmf.bimhui.com/TimeFlow_Human_app/74c84b0455827e99bc963913a3442ae8.png" /> <text class="font text_6 ml 5">开启新对话</text> </view> <view class="flex row items center equal division item ml 22"> <image class="shrink 0 image_10" src="https://tmf.bimhui.com/TimeFlow_Human_app/54fced3b0dfdf97e41d28a22e80bda01.png" /> <text class="font text_7 ml 5">历史记录</text> </view> <view class="flex row equal division item_2 ml 22"> <text class="font">使用次数剩</text> <text class="font text_8 ml 4">4次</text> </view> </view> <view class="flex row justify between items center section_4 pos_18"> <image class="image_12" src="https://tmf.bimhui.com/TimeFlow_Human_app/9ca79863e79a4ce31476ff029a0a5b68.png" @click="changeInputType" /> <text class="text_9" v if="inputType === 'voice'">按住说话</text> <input class="input_13" v else type="text" placeholder="请输入您的问题..." v model="inputMessage" /> <image class="image_11" src="https://tmf.bimhui.com/TimeFlow_Human_app/04e19c9283df0c50973ff6e885bc421f.png" @click="sendMessage" /> </view> <view class="section_5 pos_19"></view> </view>
</template> <script>
import template_page_mixin from '@/libs/mixin/template_page_mixin.js';
export default { mixins: [template_page_mixin], components: , props: , data() { return { inputType: 'text', count: 4, inputMessage: '', // 用户输入的消息 isAiTyping: false, // AI是否正在输入 scrollToViewId: '', // 滚动定位ID messageList: [ { type: 'user', content: '如何提取视频内容', image: 'https://tmf.bimhui.com/TimeFlow_Human_app/a07479c17fa5e2042bf9439d5cae65b5.png' }, { type: 'ai', content: '当然可以。提取短视频内容是一个广泛的需求,可能指下载保存、分析内容(如文案、音乐、数据)或进行二次创作。我将从这几个方面为您提供详细的方法和注意事项。核心概念:提取 ≠ 盗用.首先必须明确,提取内容用于个人学习、欣赏或合理引用是常见的,但未经授权直接盗用他人视频并声称原创是侵权行为,请务必遵守平台规则和版权法律。', image: 'https://tmf.bimhui.com/TimeFlow_Human_app/a07479c17fa5e2042bf9439d5cae65b5.png' }, { type: 'user', content: '提取文案', image: 'https://tmf.bimhui.com/TimeFlow_Human_app/a07479c17fa5e2042bf9439d5cae65b5.png' } ] }; }, methods: { changeInputType() { this.inputType = this.inputType === 'text' ? 'voice' : 'text'; }, copyText(text) { uni.setClipboardData({ data: text, success: () => { uni.showToast({ title: '复制成功', icon: 'success', duration: 2000 }); } }); }, sendMessage() { // 检查输入是否为空 if (!this.inputMessage.trim()) { return; } // 添加用户消息到消息列表 const userMessage = { type: 'user', content: this.inputMessage.trim(), image: 'https://tmf.bimhui.com/TimeFlow_Human_app/a07479c17fa5e2042bf9439d5cae65b5.png' }; this.messageList.push(userMessage); // 清空输入框 const messageToProcess = this.inputMessage.trim(); this.inputMessage = ''; // 滚动到底部 this.scrollToBottom(); // 触发AI回复 this.generateAiResponse(messageToProcess); }, generateAiResponse(userMessage) { // 设置AI正在输入状态 this.isAiTyping = true; // 模拟AI思考时间 setTimeout(() => { const aiResponse = this.getAiResponse(userMessage); // 添加AI回复到消息列表 const aiMessage = { type: 'ai', content: aiResponse, image: 'https://tmf.bimhui.com/TimeFlow_Human_app/a07479c17fa5e2042bf9439d5cae65b5.png' }; this.messageList.push(aiMessage); this.isAiTyping = false; // 滚动到底部 this.scrollToBottom(); }, 1000 + Math.random() * 2000); // 1 3秒随机延迟 }, getAiResponse(userMessage) { // 简单的关键词匹配回复逻辑 const responses = { '视频': '关于视频处理,我可以帮您分析视频内容、提取关键信息、生成文案等。您具体需要什么帮助呢?', '文案': '我可以帮您创作各种类型的文案,包括短视频文案、营销文案、产品描述等。请告诉我您的具体需求。', '抖音': '抖音内容创作需要注意热点话题、用户喜好和平台算法。我可以帮您分析热门内容趋势和创作技巧。', '提取': '内容提取可以包括文字、音频、视频等多个维度。请具体说明您想要提取什么类型的内容?', '你好': '您好!我是您的AI助手,专门帮助您进行内容创作和分析。有什么可以为您服务的吗?', '帮助': '我可以帮您:\n1. 分析视频内容\n2. 生成创意文案\n3. 提供创作建议\n4. 解答相关问题\n\n请告诉我您的具体需求!' }; // 检查是否包含关键词 for (let keyword in responses) { if (userMessage.includes(keyword)) { return responses[keyword]; } } // 默认回复 return `我理解您提到了"${userMessage}"。作为您的AI助手,我会尽力为您提供帮助。请您详细描述一下您的需求,这样我就能给出更准确的建议和解决方案。`; }, scrollToBottom() { this.$nextTick(() => { // 使用scroll into view滚动到底部 this.scrollToViewId = ''; this.$nextTick(() => { this.scrollToViewId = 'scroll bottom'; }); }); } },
};
</script> <style scoped lang="scss">
@import '@/static/css/templatePage/custom_nav_bar.scss'; .ml 5 { margin left: 10rpx;
} .page { background color: #ffffff; background image: linear gradient(167.6deg, #181629 13.9%, #272144 58.2%); width: 100%; overflow y: auto; overflow x: hidden; height: 100vh; padding bottom: 240rpx; .section { background image: linear gradient(167.6deg, #181629 13.9%, #272144 58.2%); width: 750rpx; height: 1624rpx; } // 对话容器样式 .chat container { padding: 32rpx; height: 100%; } // 消息项容器 .message item { margin bottom: 32rpx; } // 用户消息容器 右对齐 .user message container { display: flex; flex direction: row; justify content: flex end; align items: flex start; width: 100%; .user avatar { width: 80rpx; height: 80rpx; border radius: 50%; margin left: 16rpx; flex shrink: 0; } .user message content { display: flex; justify content: flex end; max width: 70%; .user message bubble { padding: 20rpx 30rpx; background color: #6c5ce7; border radius: 30rpx 8rpx 30rpx 30rpx; border: solid 2rpx #9a92e9; max width: 100%; .user message text { color: #ffffff; font size: 28rpx; font weight: 500; line height: 40rpx; word wrap: break word; } } } } // AI消息容器 左对齐 .ai message container { display: flex; flex direction: row; justify content: flex start; align items: flex start; width: 100%; .ai avatar { width: 80rpx; height: 80rpx; border radius: 50%; margin right: 16rpx; flex shrink: 0; } .ai message content { display: flex; justify content: flex start; max width: 70%; .ai message bubble { padding: 20rpx 30rpx; background color: rgba(255, 255, 255, 0.1); border radius: 8rpx 30rpx 30rpx 30rpx; border: solid 1rpx rgba(255, 255, 255, 0.2); backdrop filter: blur(10rpx); max width: 100%; .ai message text { color: #ffffff; font size: 28rpx; font weight: 400; line height: 40rpx; word wrap: break word; margin bottom: 16rpx; } // 操作按钮区域 .action buttons { display: flex; flex direction: row; gap: 16rpx; margin top: 16rpx; .action item { display: flex; flex direction: row; align items: center; padding: 8rpx 16rpx; background color: rgba(255, 255, 255, 0.1); border radius: 20rpx; border: solid 1rpx rgba(255, 255, 255, 0.2); transition: all 0.3s ease; &:hover { background color: rgba(255, 255, 255, 0.2); } .action icon { width: 24rpx; height: 24rpx; margin right: 8rpx; } } } } } } .scroll anchor { height: 1rpx; width: 100%; } .font_2 { font size: 24rpx; font family: PingFang SC; line height: 28rpx; font weight: 700; color: #ffffff; } .font_3 { font size: 20rpx; font family: PingFang SC; line height: 18.46rpx; color: #61ddfd; } .text_5 { line height: 18.48rpx; } .equal division { position: fixed; left: 30rpx; right: 32rpx; bottom: 180rpx; z index: 999; .equal division item_1 { padding: 13rpx 20.6rpx 13rpx 24rpx; background color: #2d2756; border radius: 1000rpx; height: 58rpx; .text_6 { line height: 22.4rpx; } } .equal division item { padding: 13rpx 21.08rpx 13rpx 24rpx; background color: #2d2756; border radius: 1000rpx; height: 58rpx; .text_7 { line height: 21.98rpx; } } .image_10 { width: 32rpx; height: 32rpx; } .equal division item_2 { padding: 18.2rpx 10.72rpx 13.44rpx 24.74rpx; background image: linear gradient(90deg, #8ffe67 8.8%, #2d2756 49.6%, #4cd9fd 108.1%); border radius: 1000rpx; height: 58rpx; border: solid 2rpx #ffffff33; .text_8 { line height: 22.36rpx; } } } .font { font size: 24rpx; font family: PingFang SC; line height: 22.28rpx; color: #ffffff; } .section_4 { padding: 32rpx 24rpx 24rpx 34rpx; background color: #261e53; border radius: 1000rpx; border: solid 4rpx #493eb0; .image_12 { border radius: 2rpx 2rpx 6rpx 6rpx; width: 44rpx; height: 36rpx; } .text_9 { color: #ffffff; font size: 28rpx; font family: PingFang SC; font weight: 800; line height: 26.44rpx; opacity: 0.6; } .input_13 { margin left: 20rpx; flex: 1; background: transparent; border: none; outline: none; color: #ffffff; font size: 28rpx; font family: PingFang SC; font weight: 500; line height: 26.44rpx; &::placeholder { color: rgba(255, 255, 255, 0.6); font size: 28rpx; font family: PingFang SC; font weight: 400; } } .image_11 { width: 48rpx; height: 48rpx; } } .pos_18 { position: fixed; left: 30rpx; right: 32rpx; bottom: 40rpx; z index: 999; } .section_5 { background color: #231f3d; width: 750rpx; height: 68rpx; } .pos_19 { position: fixed; left: 0; right: 0; bottom: 0; z index: 998; } .chat input container { position: fixed; left: 30rpx; right: 30rpx; bottom: 120rpx; z index: 1000; background color: rgba(35, 31, 61, 0.95); border radius: 25rpx; padding: 20rpx; backdrop filter: blur(10rpx); border: 1rpx solid rgba(255, 255, 255, 0.1); .input wrapper { display: flex; flex direction: row; align items: center; gap: 20rpx; .chat input { flex: 1; background color: rgba(255, 255, 255, 0.1); border: 1rpx solid rgba(255, 255, 255, 0.2); border radius: 20rpx; padding: 20rpx 30rpx; color: #ffffff; font size: 28rpx; line height: 40rpx; &::placeholder { color: rgba(255, 255, 255, 0.6); } } .send button { background color: rgba(255, 255, 255, 0.2); border radius: 20rpx; padding: 20rpx 30rpx; transition: all 0.3s ease; &.active { background color: #6c5ce7; } .send text { color: #ffffff; font size: 28rpx; font weight: 600; } } } } .ai typing indicator { display: flex; flex direction: row; align items: flex start; margin: 30rpx 0; padding: 0 30rpx; .ai avatar { width: 80rpx; height: 80rpx; border radius: 50%; margin right: 20rpx; flex shrink: 0; } .typing content { background color: rgba(255, 255, 255, 0.1); border radius: 20rpx; padding: 20rpx 30rpx; border: 1rpx solid rgba(255, 255, 255, 0.2); display: flex; flex direction: column; gap: 10rpx; .typing text { color: rgba(255, 255, 255, 0.8); font size: 24rpx; } .typing dots { display: flex; flex direction: row; gap: 8rpx; .dot { width: 8rpx; height: 8rpx; background color: #6c5ce7; border radius: 50%; animation: typing 1.4s infinite ease in out; &:nth child(1) { animation delay: 0.32s; } &:nth child(2) { animation delay: 0.16s; } } } } } @keyframes typing { 0%, 80%, 100% { transform: scale(0); opacity: 0.5; } 40% { transform: scale(1); opacity: 1; } }
}
</style>
uniapp实现仿网易云播放器页面(已实现自动获取音乐时长,上下首,循环播放,拖拽快进后退)(2025.7.18更新版实现真机效果)
分类: 前端
简介:因公司业务需求,要求我在小程序中增加一个播放器,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>
uniapp微信小程序接入微信云函数(文本检测和图片检测篇)
分类: 前端
简介:参考文档链接:csdn使用说明:文章中的用法是放在图片和文本上传中,我的用处是微信授权头像和昵称后记:感觉自己nc了,微信头像本来就是经过验证的,多过一遍没有意义,具体问题下方会说明。代码实例: <template> <view v if="openModal" class="wx authorization modal"> <view class="wam__mask" @touchmove.prevent="" @tap.stop="closeModal"></view> <! 内容区域 > <view class="wam__wrapper"> <! 关闭按钮 > <view class="wam__close btn" @tap.stop="closeModal"> <text class="tn icon close"></text> </view> <! 标题 > <view class="wam__title">获取您的昵称、头像</view> <! tips > <view class="wam__sub title"> 获取用户头像、昵称,主要用于向用户提供具有辨识度的用户中心界面 </view> <! 头像选择 > <view class="wam__avatar"> <view class="button shadow"> <button class="button" open type="chooseAvatar" @chooseavatar="chooseAvatarEvent"> <view v if="userInfo.avatar" class="avatar__image"> <image class="image" :src="userInfo.avatar" mode="aspectFill"></image> </view> <view v else class="avatar__empty"> <image class="image" src="https://cdn.nlark.com/yuque/0/2022/jpeg/280373/1668928062708 assets/web upload/764843cf 055a 4cb6 b5d3 dca528b33fd4.jpeg" mode="aspectFill"></image> </view> <view class="avatar icon"> <view class="tn icon camera fill"></view> </view> </button> </view> </view> <! 昵称输入 > <view class="wam__nickname"> <view class="nickname__data"> <input class="input" type="nickname" v model="inputNickname" placeholder="请输入昵称" placeholder style="color: #AAAAAA;" @change="handleNicknameChange"> </view> </view> <! 保存按钮 > <view class="wam__submit btn" :class="[{ 'disabled': !userInfo.avatar || !userInfo.nickname }]" hover class="tn btn hover class" :hover stay time="150" @tap.stop="submitUserInfo"> 保 存 </view> </view> </view>
</template> <script> export default { options: { // 在微信小程序中将组件节点渲染为虚拟节点,更加接近Vue组件的表现(不会出现shadow节点下再去创建元素) virtualHost: true }, props: { value: { type: Boolean, default: false } }, data() { return { openModal: false, userInfo: { avatar: '', nickname: '' }, inputNickname: '' } }, watch: { value: { handler(val) { this.openModal = val }, immediate: true }, }, methods: { //选择昵称 handleNicknameChange(e) { const newNickname = e.detail.value; // 保存当前上下文的 this var that = this; wx.cloud.init({ env: "", }) if(newNickname){ console.log(newNickname) wx.cloud.callFunction({ name: 'checkTextSec', data: { content:newNickname }, success: (res) => { if (res.result.errCode === 0) { if (res.result.result.label === 100) { console.log('合法文本') that.userInfo.nickname = newNickname; } else { console.log('违法文本') } } else { console.log('其他异常') } }, fail: (e) => { console.log(e) } }) } }, // 头像选择 chooseAvatarEvent(e) { var imageUrl = e.detail.avatarUrl console.log(imageUrl) // 保存当前上下文的 this var that = this; //调用云函数checkIploadInfo,检测头像 uni.getImageInfo({ src: imageUrl, success: function(info) { // 使用 uni.request 获取图片数据 uni.request({ url: imageUrl, responseType: 'arraybuffer', success: function(response) { // 图片数据转换为 ArrayBuffer 格式的二进制数据 var arrayBuffer = response.data; wx.cloud.init({ env: "", }) wx.cloud.callFunction({ name: "checkUploadInfo", data: { contentType: `image/jpeg`, arrayBuffer: arrayBuffer }, success: (res) => { if (res.result.errCode === 0) { console.log('合法图片') that.userInfo.avatar = e.detail.avatarUrl } else if (res.result.errCode === 87014) { console.log('违法图片') uni.showToast({ title: '违规图片,请重新上传', icon: 'error', duration: 2000 }); } }, fail: () => { console.log('检测失败') uni.showToast({ title: '上传失败,请重新选择图片', icon: 'error', duration: 2000 }); } }) }, fail: function(error) { console.error("加载图片失败:", error); } }); }, fail: function(error) { console.error("获取图片信息失败:", error); } }); }, // 更新用户信息 submitUserInfo() { // 判断是否已经选择了用户头像和输入了用户昵称 if (!this.userInfo.avatar || !this.userInfo.nickname) { return uni.showToast({ icon: 'none', title: '请选择头像和输入用户信息' }) } // 更新完成事件 this.$emit('updated', this.userInfo) }, // 关闭弹框 closeModal() { this.$emit('input', false) }, } }
</script>使用的是图鸟的微信授权组件,实际使用后发现了几个问题:首先第一个头像授权,我添加头像验证的原因就是会涉及到上传本地图片,然后实际实验之后发现如果是违规图片,压根不会触发@chooseavatar,会报错,没有任何返回值;而合规图片就可以正常触发,得到返回值。查看了请求接口,并不是本地上传接口,应该是先上传到微信那边检测没过。第二个文本检测,需要使用@change事件,其他事件都无法获得值;并且change会调用两次,第一次无返回值,第二次才会有值,不做判断会浪费验证次数。