liming 1 nedēļu atpakaļ
vecāks
revīzija
f200c4ec91

+ 6 - 2
src/views/im/chat/index.vue

@@ -64,7 +64,7 @@
                 class="audio-message"
                 v-else-if="item.contentType === MSG_TYPE.AUDIO"
               >
-                <audio
+                <!-- <audio
                   v-if="item.localUrl"
                   :src="item.localUrl"
                   controls
@@ -75,7 +75,9 @@
                   :src="IM_PATH + item.url"
                   controls
                   style="width: 200px"
-                />
+                /> -->
+                <wechatAudio  v-if="item.localUrl" :src="item.localUrl"/>
+                <wechatAudio  v-else :src="IM_PATH + item.url"/>
               </div>
 
               <!-- 其他未知类型 -->
@@ -171,6 +173,8 @@ import { useWalletStore } from "@/stores/modules/walletStore.js";
 import { Keyboard } from "@capacitor/keyboard";
 import { Capacitor } from "@capacitor/core";
 import { MSG_TYPE, MESSAGE_TYPE_USER } from "@/common/constant/msgType";
+import  wechatAudio from "@/views/im/components/wechatAudio/index.vue";
+
 
 const IM_PATH = import.meta.env.VITE_IM_PATH_FIlE;
 // 路由 & store

+ 219 - 0
src/views/im/components/wechatAudio/index.vue

@@ -0,0 +1,219 @@
+<template>
+  <div class="voice-message" @click="togglePlay">
+    <!-- 波形动画 -->
+    <div class="wave">
+      <span 
+        v-for="i in 5"
+        :key="i"
+        class="bar"
+        :style="{
+          height: isPlaying ? `${randomHeights[i-1]}px` : `4px`
+        }"
+      ></span>
+    </div>
+    
+    <!-- 时长显示 -->
+    <span class="duration">{{ computedDuration }}″</span>
+    
+    <!-- 隐藏的audio元素 -->
+    <audio
+      ref="audioEl"
+      :src="src"
+      preload="metadata"
+      @loadedmetadata="handleMetadata"
+      @ended="handleEnded"
+      @error="handleError"
+    ></audio>
+  </div>
+</template>
+
+<script setup> 
+
+const props = defineProps({
+  src: String,
+  isSender: Boolean
+})
+
+const audioEl = ref(null)
+const isPlaying = ref(false)
+const rawDuration = ref(0)
+const randomHeights = ref([6, 9, 12])
+const animationFrame = ref(null)
+
+// 计算后的时长(处理Infinity情况)
+const computedDuration = computed(() => {
+  return isFinite(rawDuration.value) ? Math.round(rawDuration.value) : '--'
+})
+
+// 初始化加载
+onMounted(() => {
+  loadAudio()
+})
+
+// 监听URL变化
+watch(() => props.src, () =>{
+  loadAudio()
+})
+
+// 加载音频
+const loadAudio = async () => {
+  if (!props.src) return
+  
+  try {
+    // 重置audio元素
+    if (audioEl.value) {
+      audioEl.value.pause()
+      audioEl.value.currentTime = 0
+      audioEl.value.src = props.src
+      await audioEl.value.load()
+    }
+    
+    // 备用方案:如果loadedmetadata未触发
+    setTimeout(() => {
+      if (rawDuration.value === 0) {
+        getDurationFallback()
+      }
+    }, 500)
+  } catch (err) {
+    console.error('音频加载失败:', err)
+    getDurationFallback()
+  }
+}
+
+// 正常获取时长
+const handleMetadata = () => {
+  if (audioEl.value && isFinite(audioEl.value.duration)) {
+    rawDuration.value = audioEl.value.duration
+  } else {
+    getDurationFallback()
+  }
+}
+
+// 备用方案:使用Web Audio API
+const getDurationFallback = async () => {
+  try {
+    const dur = await getAudioDuration(props.src)
+    if (dur > 0) rawDuration.value = dur
+  } catch (err) {
+    console.error('备用方案获取时长失败:', err)
+  }
+}
+
+// Web Audio API实现
+const getAudioDuration = (url) => {
+  return new Promise((resolve) => {
+    if (!url) return resolve(0)
+    
+    const audioContext = new (window.AudioContext || window.webkitAudioContext)()
+    const request = new XMLHttpRequest()
+    
+    request.open('GET', url, true)
+    request.responseType = 'arraybuffer'
+    
+    request.onload = () => {
+      audioContext.decodeAudioData(
+        request.response,
+        (buffer) => resolve(buffer.duration),
+        () => resolve(0)
+      )
+    }
+    
+    request.onerror = () => resolve(0)
+    request.send()
+  })
+}
+
+// 播放控制
+const togglePlay = () => {
+  if (!audioEl.value) return
+  
+  if (isPlaying.value) {
+    audioEl.value.pause()
+  } else {
+    audioEl.value.play()
+  }
+  isPlaying.value = !isPlaying.value
+  updateWaveAnimation()
+}
+
+// 波形动画
+const updateWaveAnimation = () => {
+  if (isPlaying.value) {
+    cancelAnimationFrame(animationFrame.value)
+    const animate = () => {
+      randomHeights.value = [
+        Math.random() * 9 + 6,
+        Math.random() * 12 + 6,
+        Math.random() * 9 + 6,
+        Math.random() * 12 + 6,
+        Math.random() * 9 + 6,
+      ]
+      animationFrame.value = requestAnimationFrame(animate)
+    }
+    animate()
+  } else {
+    cancelAnimationFrame(animationFrame.value)
+    randomHeights.value = [6, 9, 12]
+  }
+}
+
+// 音频结束处理
+const handleEnded = () => {
+  isPlaying.value = false
+  updateWaveAnimation()
+}
+
+// 错误处理
+const handleError = () => {
+  console.error('音频播放错误')
+  isPlaying.value = false
+  updateWaveAnimation()
+}
+
+// 清理资源
+onUnmounted(() => {
+  cancelAnimationFrame(animationFrame.value)
+  if (audioEl.value) {
+    audioEl.value.pause()
+    audioEl.value.src = ''
+    audioEl.value.remove()
+  }
+})
+</script>
+
+<style lang="less" scoped>
+.voice-message {
+  display: inline-flex;
+  align-items: center;
+  padding: 8px 12px;
+  border-radius: 5px;
+  background-color: v-bind("isSender ? '#E5F7FF' : '#F5F5F5'");
+  cursor: pointer;
+  user-select: none;
+  max-width: 200px;
+}
+
+.wave {
+  display: flex;
+  align-items: flex-end;
+  height: 100%;
+  margin: 0 8px;
+  gap: 3px;
+  align-items: center;
+}
+
+.bar {
+  display: flex;
+  width: 2.5px;
+  background-color:  v-bind("isSender ? '#07C160' : '#2B87EB'");
+  border-radius: 2px;
+  transition: height 0.15s ease-out;
+}
+
+.duration {
+  font-size: 12px;
+  color: #666;
+  min-width: 20px;
+  text-align: center;
+}
+</style>