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