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"
/>
相关推荐
前端不太难1 小时前
从 Navigation State 反推架构腐化
前端·架构·react
前端程序猿之路1 小时前
Next.js 入门指南 - 从 Vue 角度的理解
前端·vue.js·语言模型·ai编程·入门·next.js·deepseek
大布布将军2 小时前
⚡️ 深入数据之海:SQL 基础与 ORM 的应用
前端·数据库·经验分享·sql·程序人生·面试·改行学it
川贝枇杷膏cbppg2 小时前
Redis 的 RDB 持久化
前端·redis·bootstrap
D_C_tyu2 小时前
Vue3 + Element Plus | el-table 表格获取排序后的数据
javascript·vue.js·elementui
JIngJaneIL2 小时前
基于java+ vue农产投入线上管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot
天外天-亮3 小时前
v-if、v-show、display: none、visibility: hidden区别
前端·javascript·html
jump_jump3 小时前
手写一个 Askama 模板压缩工具
前端·性能优化·rust
hellotutu3 小时前
vue2 从 sessionStorage 手动取 token 后,手动加入到 header
vue.js·token·session·header
be or not to be3 小时前
HTML入门系列:从图片到表单,再到音视频的完整实践
前端·html·音视频