vue3+ts封装类似于微信消息的组件

组件代码如下:

html 复制代码
<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 }}''&nbsp;</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 }}&nbsp;''</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>

使用时:

html 复制代码
<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"
/>
相关推荐
Damai7 小时前
DOMPurify 完整介绍
前端·面试
随风一样自由7 小时前
【前端+跨域】跨域问题全面解析与解决方案指南
前端·跨域
小帅不太帅7 小时前
我随手发了几张截图,白拿了一个月 AI 顶配会员
前端·github
36岁的我开始学习打篮球7 小时前
外规内化技术架构
前端
雪隐8 小时前
用Flutter做背单词APP-03为了给单词库塞满数据,我让AI给我打了上万份工
前端·人工智能·后端
hunterandroid8 小时前
Android 安全最佳实践:从数据存储到网络通信的防护思路
前端
H Journey8 小时前
web开发学习:html、css、js
前端·css·html·js
hunterandroid8 小时前
Compose 性能优化:从卡顿到丝滑的实战经验
前端
hunterandroid8 小时前
Kotlin Coroutines 在 Android 项目中的落地实战
前端
不一样的少年_8 小时前
不用框架,手搓 AI Agent:(一) 先让它跑起来
前端·后端·agent