效果:
组件:
<!--聊天页面-播放语言组件-->
<template>
<view :class="['voice-message', { 'sent': !isSent, 'received': isSent }]"
@click="togglePlayback">
<!-- 默认情况下的样式,属于是接收者看到的样式,会显示在聊天框的最左侧。如果isSents true的时候,需要把【voice-icon】里面的整体翻转过来显示-->
<!-- 最大宽度为147px {60:294 === 10:49} -->
<!-- 语言条要按照语音时长显示不同的宽度,所以增加了一块宽度,发送者的时候,加在左侧,接收者的时候,加在右侧-->
<view v-if="isSent" :style="`width:${duration / 10 * 49}rpx`"></view>
<text class="duration" v-if="isSent">{{ duration }}'' </text>
<view :class="['voice-icon', { 'sent-icon': isSent }]">
<view :class="['small']" :style="smallStyle"></view>
<view :class="['middle', { 'animate': isPlaying }]" :style="middleStyle"></view>
<view :class="['large', { 'animate': isPlaying }]" :style="largeStyle"></view>
</view>
<text class="duration" :style="{color:iconColor}" v-if="!isSent">{{ duration }} ''</text>
<!-- 最大宽度为147px {60:294 === 10:49} -->
<view v-if="!isSent" :style="`width:${duration / 10 * 49}rpx`"></view>
</view>
</template>
<script setup>
import { ref, watch, computed,onUnmounted,defineEmits } from 'vue';
// 从 props 中获取动态参数
const props = defineProps({
isSent: {
type: Boolean,//是不是发送者?两者是对称反转的样式
default: false
},
iconColor: {
type: String,
default: '#FFFFFF'
},
smallSize: {
type: Number,
default: 10
},
middleSize: {
type: Number,
default: 20
},
largeSize: {
type: Number,
default: 30
},
duration: {
type: String,
default: '00'
},
audioSrc: {
type: String,
default: ''
}
});
const isPlaying = ref(false);
let innerAudioContext = null;
// 定义 emit 事件
const emit = defineEmits(['update:playStatus']);
// 计算动态样式
const smallStyle = computed(() => ({
color: props.iconColor,
width: `${props.smallSize}rpx`,
height: `${props.smallSize}rpx`,
marginRight: '-10rpx' // 调整间距
}));
const middleStyle = computed(() => ({
color: props.iconColor,
width: `${props.middleSize}rpx`,
height: `${props.middleSize}rpx`,
marginRight: '-30rpx' // 调整间距
}));
const largeStyle = computed(() => ({
color: props.iconColor,
width: `${props.largeSize}rpx`,
height: `${props.largeSize}rpx`,
marginRight: '1rpx' // 调整间距
}));
// 切换播放状态的函数
function togglePlayback() {
if (isPlaying.value) {
pauseVoice();
} else {
playVoice(props.audioSrc);
}
isPlaying.value = !isPlaying.value;
emit('update:playStatus', isPlaying.value); // 触发事件,返回当前播放状态
}
// 播放音频的函数
function playVoice(voiceSrc) {
console.log('播放录音', voiceSrc);
// 如果音频上下文不存在,则创建新的音频上下文
if (!innerAudioContext) {
innerAudioContext = uni.createInnerAudioContext();
}
// 设置音频源
innerAudioContext.src = voiceSrc;
// 播放音频
innerAudioContext.play();
// 监听播放结束事件
innerAudioContext.onEnded(() => {
isPlaying.value = false;
});
}
// 暂停音频的函数
function pauseVoice() {
if (innerAudioContext) {
innerAudioContext.pause();
}
}
// 组件卸载时销毁音频上下文
onUnmounted(() => {
if (innerAudioContext) {
innerAudioContext.destroy();
innerAudioContext = null;
}
});
</script>
<style scoped>
.voice-message {
display: flex;
align-items: center;
cursor: pointer;
}
.voice-message.sent {
justify-content: flex-end;
}
.voice-message.received {
justify-content: flex-start;
}
.voice-icon {
display: flex;
align-items: center;
}
.voice-icon.sent-icon {
transform: rotate(180deg);
}
.small, .middle, .large {
border-style: solid;
border-top-color: transparent;
border-left-color: transparent;
border-bottom-color: transparent;
border-radius: 50%;
box-sizing: border-box;
vertical-align: middle;
display: inline-block;
background-color: transparent; /* 默认背景颜色为透明 */
}
.middle.animate {
animation: show2 3s ease-in-out infinite;
}
.large.animate {
animation: show3 3s ease-in-out infinite;
}
@keyframes show2 {
0% { opacity: 0; }
30% { opacity: 1; }
100% { opacity: 0; }
}
@keyframes show3 {
0% { opacity: 0; }
60% { opacity: 1; }
100% { opacity: 0; }
}
.duration {
margin-left: 8px;
font-size: 32rpx;
color: #FFFFFF;
font-weight: 400;
}
</style>
使用组件:
<VoicePlayer
:isSent="false"
iconColor="#333333"
:smallSize="5"
:middleSize="30"
:largeSize="48"
:duration="item.voice_time"
:audioSrc="item.voice"
v-model:playStatus="playStatus[item.message_id]"
@update:playStatus="handlePlayStatusChange(item.message_id, $event)"
/>