Files
ks-app-employment-service/pages/chat/components/WaveDisplay.vue
2025-04-07 09:10:55 +08:00

274 lines
6.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>