组件代码如下:
<template>
<div
:class="['voice-message', { sent: isSent, received: !isSent }]"
:style="{ backgroundColor: backgroundColor }"
@click="togglePlayback"
>
<!-- isSent为false在左侧,为true在右侧-->
<!-- 语言条要按照语音时长显示不同的宽度,所以增加了一块宽度,发送者的时候,加在左侧,接收者的时候,加在右侧-->
<div v-if="isSent" :style="`width:${(duration / 10) * 30}px`"></div>
<span class="duration" v-if="isSent">{{ duration }}'' </span>
<div :class="['voice-icon', { 'sent-icon': isSent }]">
<div :class="['small']" :style="smallStyle"></div>
<div :class="['middle', { animate: isPlaying }]" :style="middleStyle"></div>
<div :class="['large', { animate: isPlaying }]" :style="largeStyle"></div>
</div>
<span class="duration" :style="{ color: iconColor }" v-if="!isSent">{{ duration }} ''</span>
<div v-if="!isSent" :style="`width:${(duration / 10) * 30}px`"></div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, withDefaults, onBeforeUnmount } from "vue";
// 使用 withDefaults 提供默认值
const props = withDefaults(
defineProps<{
isSent?: boolean;
iconColor?: string;
backgroundColor?: string;
smallSize?: number;
middleSize?: number;
largeSize?: number;
duration?: number;
audioSrc?: string;
}>(),
{
isSent: false,
iconColor: "#000000",
backgroundColor: "",
smallSize: 10,
middleSize: 20,
largeSize: 30,
duration: 0,
audioSrc: ""
}
);
const isPlaying = ref(false);
let audio: HTMLAudioElement | null = null;
// 计算动态样式
const smallStyle = computed(() => ({
color: props.iconColor,
width: `${props.smallSize}px`,
height: `${props.smallSize}px`,
marginRight: -props.smallSize + "px"
}));
const middleStyle = computed(() => ({
color: props.iconColor,
width: `${props.middleSize}px`,
height: `${props.middleSize}px`,
marginRight: -props.middleSize + "px"
}));
const largeStyle = computed(() => ({
color: props.iconColor,
width: `${props.largeSize}px`,
height: `${props.largeSize}px`,
marginRight: "1px"
}));
// 切换播放状态的函数
const togglePlayback = () => {
if (isPlaying.value) {
pauseVoice();
} else {
playVoice(props.audioSrc || "");
}
};
// 播放音频的函数
const playVoice = (voiceSrc: string) => {
if (!voiceSrc) {
console.error("音频源不能为空");
return;
}
// 如果音频上下文不存在,则创建新的 HTMLAudioElement
if (!audio) {
audio = new Audio(voiceSrc);
} else {
audio.src = voiceSrc;
}
isPlaying.value = true;
// 播放音频
audio.play().catch(error => console.error("音频播放失败", error));
// 监听播放结束事件
audio.onended = () => {
isPlaying.value = false;
};
};
// 暂停音频的函数
const pauseVoice = () => {
isPlaying.value = false;
if (audio) {
audio.pause();
}
};
// 组件卸载时销毁音频上下文
onBeforeUnmount(() => {
if (audio) {
audio.pause();
audio = null;
}
});
defineExpose({
pauseVoice
});
</script>
<style scoped>
.voice-message {
display: inline-flex;
align-items: center;
cursor: pointer;
border-radius: 10px;
padding: 4px 12px;
}
.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: 20px;
color: #ffffff;
font-weight: 400;
}
</style>
使用时:
<VoicePlayback
:isSent="false"
iconColor="#ffffff"
backgroundColor="rgba(255 255 255 / 20%)"
:smallSize="5"
:middleSize="16"
:largeSize="28"
:duration="30"
audioSrc="http://music.163.com/song/media/outer/url?id=447925558.mp3"
/>