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 分钟前
Scrape Webpack练习
开发语言·javascript·webpack
程序员黑豆22 分钟前
Windows 系统 Java 环境变量配置全攻略:解决“不是内部或外部命令”
java·前端·ai编程
To_OC38 分钟前
别只拿 useRef 绑 DOM 了,搭配 Web Worker 解决页面卡顿才是真的香
前端·react.js·dom
IT_陈寒2 小时前
Vue的响应式让我原地破防,原来问题出在这
前端·人工智能·后端
子兮曰2 小时前
Bun vs Node.js 深度对决:跑分快 4 倍,真实业务只剩 3%,2026 年到底该怎么选?
前端·后端·typescript
计算机魔术师2 小时前
终端用户
前端
码事漫谈3 小时前
把 AI 拆掉,你的系统还能跑吗?
前端·后端
默_笙3 小时前
⛵ 我用 React + TS 做了个"调色盘",顺便学会了企业级项目的目录架构
前端·javascript
预知同行4 小时前
从 MCP 到 CLI:AI Agent 工具链的架构演进与实战抉择
前端·面试
何智超4 小时前
AI 微前端性能优化之旅(中):重启
前端·vibecoding