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;
|
2025-04-16 14:24:06 +08:00
|
|
|
|
|
|
|
|
|
const AMPLIFY = 1.6; // 振幅放大
|
|
|
|
|
const center = centerIndex.value;
|
|
|
|
|
|
|
|
|
|
// 如果没有传入音频数据,则使用模拟数据(加强振幅)
|
2025-04-07 09:10:55 +08:00
|
|
|
|
const audioData =
|
|
|
|
|
props.audioData.length > 0
|
2025-04-16 14:24:06 +08:00
|
|
|
|
? props.audioData.map((v) => Math.min(v * AMPLIFY, 1))
|
|
|
|
|
: Array(center + 1)
|
2025-04-07 09:10:55 +08:00
|
|
|
|
.fill(0)
|
2025-04-16 14:24:06 +08:00
|
|
|
|
.map(() => Math.random() * 0.7 + 0.3); // 模拟值更明显
|
2025-04-07 09:10:55 +08:00
|
|
|
|
|
2025-04-16 14:24:06 +08:00
|
|
|
|
for (let i = 0; i <= center; i++) {
|
|
|
|
|
const leftIndex = center - i;
|
|
|
|
|
const rightIndex = center + i;
|
|
|
|
|
const value = audioData[i] || 0;
|
2025-04-07 09:10:55 +08:00
|
|
|
|
|
2025-04-16 14:24:06 +08:00
|
|
|
|
if (leftIndex >= 0) updateWaveBar(leftIndex, value);
|
2025-04-07 09:10:55 +08:00
|
|
|
|
if (rightIndex < waveBars.value.length && rightIndex !== leftIndex) {
|
|
|
|
|
updateWaveBar(rightIndex, value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
animationId = requestAnimationFrame(updateWaveform);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 更新单个波形条
|
|
|
|
|
const updateWaveBar = (index, value) => {
|
2025-04-16 14:24:06 +08:00
|
|
|
|
// 动态高度 (4rpx到42rpx之间)
|
|
|
|
|
const height = 2 + value * 38;
|
2025-04-07 09:10:55 +08:00
|
|
|
|
// // 动态颜色
|
|
|
|
|
// 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>
|