Files
ks-app-employment-service/pages/chat/components/WaveDisplay.vue

274 lines
6.2 KiB
Vue
Raw Normal View History

2025-04-07 09:10:55 +08:00
<template>
<view class="wave-container active" :style="{ background }">
<!-- 波形显示区域 -->
<view class="wave">
<view
v-for="(bar, index) in waveBars"
:key="index"
class="wave-bar"
:style="{
height: `${bar.height}px`,
backgroundColor: bar.color,
borderRadius: `${bar.borderRadius}px`,
}"
></view>
</view>
</view>
</template>
<script setup>
import { ref, watch, computed, onMounted, onUnmounted } from 'vue';
const props = defineProps({
// 是否激活显示
isActive: {
type: Boolean,
default: false,
},
// 音频数据数组 (0-1之间的值)
audioData: {
type: Array,
default: () => [],
},
// 是否显示录音信息
showInfo: {
type: Boolean,
default: false,
},
// 录音时间 (秒)
recordingTime: {
type: Number,
default: 0,
},
// 是否显示取消提示
showCancelTip: {
type: Boolean,
default: false,
},
// 是否处于取消状态
isCanceling: {
type: Boolean,
default: false,
},
background: {
type: String,
default: 'linear-gradient(to right, #377dff, #9a60ff)',
},
});
const emit = defineEmits(['update:audioData']);
// 波形条数据
const waveBars = ref([]);
// 中心条索引
const centerIndex = ref(0);
// 动画帧ID
let animationId = null;
// 格式化显示时间
const formattedTime = computed(() => {
const mins = Math.floor(props.recordingTime / 60)
.toString()
.padStart(2, '0');
const secs = (props.recordingTime % 60).toString().padStart(2, '0');
return `${mins}:${secs}`;
});
// 录音提示文本
const recordingTip = computed(() => {
return props.isCanceling ? '松开取消' : '松手发送';
});
// 初始化波形条
const initWaveBars = () => {
waveBars.value = [];
// 创建31个波形条奇数个确保有真正的中心点
const barCount = 31;
centerIndex.value = Math.floor(barCount / 2);
for (let i = 0; i < barCount; i++) {
// 设置初始状态(中心条最高,向两侧递减)
const distanceFromCenter = Math.abs(i - centerIndex.value);
const initialHeight = Math.max(2, 20 - distanceFromCenter * 3);
waveBars.value.push({
height: initialHeight,
color: '#FFFFFF',
borderRadius: 2,
});
}
};
// 更新波形显示
const updateWaveform = () => {
if (!props.isActive) return;
// 如果没有传入音频数据,则使用模拟数据
const audioData =
props.audioData.length > 0
? props.audioData
: Array(centerIndex.value + 1)
.fill(0)
.map(() => Math.random() * 0.5 + 0.2);
// 从中间向两侧处理
for (let i = 0; i <= centerIndex.value; i++) {
// 左侧条索引
const leftIndex = centerIndex.value - i;
// 右侧条索引
const rightIndex = centerIndex.value + i;
// 获取音频数据值 (归一化到0-1)
const value = audioData[i] || 0;
// 更新左侧条
if (leftIndex >= 0) {
updateWaveBar(leftIndex, value);
}
// 更新右侧条(避免重复更新中心条)
if (rightIndex < waveBars.value.length && rightIndex !== leftIndex) {
updateWaveBar(rightIndex, value);
}
}
// 继续动画
animationId = requestAnimationFrame(updateWaveform);
};
// 更新单个波形条
const updateWaveBar = (index, value) => {
// 动态高度 (4rpx到200rpx之间)
const height = 2 + value * 98;
// // 动态颜色
// let color;
// if (props.isCanceling) {
// color = '#F44336'; // 取消状态显示红色
// } else {
// const intensity = Math.min(1, value * 1.5);
// const r = Math.floor(7 + intensity * 200);
// const g = Math.floor(193 - intensity * 50);
// const b = Math.floor(96 - intensity * 30);
// color = `rgb(${r}, ${g}, ${b})`;
// }
let color = '#FFFFFF';
// 动态圆角
const borderRadius = Math.min(4, height * 0.4);
waveBars.value[index] = {
height,
color,
borderRadius,
};
};
// 开始动画
const startAnimation = () => {
if (!animationId) {
animationId = requestAnimationFrame(updateWaveform);
}
};
// 停止动画
const stopAnimation = () => {
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
};
// 监听激活状态变化
watch(
() => props.isActive,
(newVal) => {
if (newVal) {
startAnimation();
} else {
stopAnimation();
}
}
);
// 组件挂载时初始化
onMounted(() => {
initWaveBars();
if (props.isActive) {
startAnimation();
}
});
// 组件卸载时清理
onUnmounted(() => {
stopAnimation();
});
</script>
<style lang="scss">
.wave-container {
position: relative;
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
padding: 16rpx;
border-radius: 36rpx;
height: calc(102rpx - 40rpx);
gap: 4rpx;
box-shadow: 0rpx 8rpx 40rpx 0rpx rgba(0, 54, 170, 0.15);
overflow: hidden;
&.active {
opacity: 1;
}
}
.wave {
width: 100%;
height: 200rpx;
display: flex;
justify-content: center;
align-items: center;
}
.wave-bar {
width: 6rpx;
min-height: 4rpx;
border-radius: 4rpx;
margin: 0 3rpx;
transition: height 0.3s ease-out, background-color 0.2s;
transform-origin: center bottom;
}
.wave-bar:nth-child(3n) {
opacity: 0.7;
}
.recording-info {
position: absolute;
bottom: -80rpx;
width: 100%;
text-align: center;
font-size: 32rpx;
color: #666;
}
.recording-tip {
font-size: 28rpx;
color: #07c160;
}
.recording-time {
font-size: 28rpx;
color: #07c160;
margin-top: 10rpx;
}
.cancel-tip {
position: absolute;
bottom: -120rpx;
width: 100%;
text-align: center;
font-size: 28rpx;
color: #f44336;
}
</style>