wkw 4 päivää sitten
vanhempi
sitoutus
ce40bb536f
3 muutettua tiedostoa jossa 164 lisäystä ja 120 poistoa
  1. 17 0
      src/assets/svg/keyboard.svg
  2. 145 120
      src/views/im/chat/index.vue
  3. 2 0
      src/views/im/contactList/invitation/index.vue

+ 17 - 0
src/assets/svg/keyboard.svg

@@ -0,0 +1,17 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="Frame" clip-path="url(#clip0_315_509)">
+<path id="Vector" d="M12 22C17.5229 22 22 17.5229 22 12C22 6.47715 17.5229 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5229 6.47715 22 12 22Z" stroke="black" stroke-width="2" stroke-linejoin="round"/>
+<path id="Vector_2" fill-rule="evenodd" clip-rule="evenodd" d="M7.5 9.5C8.19035 9.5 8.75 8.94035 8.75 8.25C8.75 7.55965 8.19035 7 7.5 7C6.80965 7 6.25 7.55965 6.25 8.25C6.25 8.94035 6.80965 9.5 7.5 9.5Z" fill="black"/>
+<path id="Vector_3" fill-rule="evenodd" clip-rule="evenodd" d="M7.5 13.5C8.19035 13.5 8.75 12.9403 8.75 12.25C8.75 11.5597 8.19035 11 7.5 11C6.80965 11 6.25 11.5597 6.25 12.25C6.25 12.9403 6.80965 13.5 7.5 13.5Z" fill="black"/>
+<path id="Vector_4" fill-rule="evenodd" clip-rule="evenodd" d="M12 9.5C12.6903 9.5 13.25 8.94035 13.25 8.25C13.25 7.55965 12.6903 7 12 7C11.3097 7 10.75 7.55965 10.75 8.25C10.75 8.94035 11.3097 9.5 12 9.5Z" fill="black"/>
+<path id="Vector_5" fill-rule="evenodd" clip-rule="evenodd" d="M12 13.5C12.6903 13.5 13.25 12.9403 13.25 12.25C13.25 11.5597 12.6903 11 12 11C11.3097 11 10.75 11.5597 10.75 12.25C10.75 12.9403 11.3097 13.5 12 13.5Z" fill="black"/>
+<path id="Vector_6" fill-rule="evenodd" clip-rule="evenodd" d="M16.5 9.5C17.1903 9.5 17.75 8.94035 17.75 8.25C17.75 7.55965 17.1903 7 16.5 7C15.8097 7 15.25 7.55965 15.25 8.25C15.25 8.94035 15.8097 9.5 16.5 9.5Z" fill="black"/>
+<path id="Vector_7" fill-rule="evenodd" clip-rule="evenodd" d="M16.5 13.5C17.1903 13.5 17.75 12.9403 17.75 12.25C17.75 11.5597 17.1903 11 16.5 11C15.8097 11 15.25 11.5597 15.25 12.25C15.25 12.9403 15.8097 13.5 16.5 13.5Z" fill="black"/>
+<path id="Vector_8" d="M8.5 16.5H15.5" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+</g>
+<defs>
+<clipPath id="clip0_315_509">
+<rect width="24" height="24" fill="white"/>
+</clipPath>
+</defs>
+</svg>

+ 145 - 120
src/views/im/chat/index.vue

@@ -7,7 +7,7 @@
     <!-- 顶部导航 -->
     <div class="header-chat">
       <svg-icon class="page-icon" name="lf-arrow" @click="goBack" />
-      <div class="header-title">群聊2({{ wsStore.messages.length }})</div>
+      <div class="header-title">{{ wsStore.toUserInfo.nickname }}</div>
       <svg-icon class="page-icon" name="more" @click="goDetail" />
     </div>
 
@@ -65,18 +65,6 @@
                 class="audio-message"
                 v-else-if="item.contentType === MSG_TYPE.AUDIO"
               >
-                <!-- <audio
-                  v-if="item.localUrl"
-                  :src="item.localUrl"
-                  controls
-                  style="width: 200px"
-                />
-                <audio
-                  v-else
-                  :src="IM_PATH + item.url"
-                  controls
-                  style="width: 200px"
-                /> -->
                 <messageAudio
                   :src="item?.localUrl || IM_PATH + item.url"
                   :isSender="isSender(item.toUsername)"
@@ -94,26 +82,39 @@
     <!-- 输入框 -->
     <div class="page-foot">
       <div class="flex-box">
+        <!-- 录音/文字切换按钮 -->
         <svg-icon
           type="button"
           class="page-icon"
-          name="voice"
-          @mousedown="startAudio"
-          @touchstart="startAudio"
-          @mouseup="sendAudioMessage"
-          @touchend="sendAudioMessage"
-        />
-        <van-field
-          rows="1"
-          type="textarea"
-          :border="false"
-          autosize
-          class="input"
-          v-model="text"
-          @focus="onFocus"
-          placeholder="输入文本"
-          @keyup.enter="sendMessage"
+          :name="voiceMode ? 'keyboard' : 'voice'"
+          @click="toggleVoiceMode"
         />
+
+        <!-- 文字输入框 或 按住说话按钮 -->
+        <template v-if="!voiceMode">
+          <van-field
+            rows="1"
+            type="textarea"
+            :border="false"
+            autosize
+            class="input"
+            v-model="text"
+            @focus="onFocus"
+            placeholder="输入文本"
+            @keyup.enter="sendMessage"
+          />
+        </template>
+
+        <template v-else>
+          <div
+            class="hold-talk-btn"
+            @touchstart.prevent.stop="handleTouchStart"
+            @touchmove.prevent.stop="handleTouchMove"
+            @touchend.prevent.stop="handleTouchEnd"
+          >
+            {{ cancelRecording ? "松开手指,取消发送" : "按住说话" }}
+          </div>
+        </template>
         <svg-icon
           class="page-icon mr12 emoji-toggle"
           name="emoji"
@@ -126,6 +127,12 @@
         />
       </div>
 
+      <!-- 录音状态浮层 -->
+      <div v-if="recording" class="recording-toast">
+        <div v-if="cancelRecording" class="cancel-msg">松开手指,取消发送</div>
+        <div v-else class="send-msg">松开发送,上滑取消</div>
+      </div>
+
       <!-- 表情面板 -->
       <div
         class="app-box"
@@ -154,11 +161,19 @@
           <div>拍摄</div>
         </div>
         <div class="tool-btn">
-          <svg-icon class="tool-icon" name="yyth" @click="startAudioOnline(Constant.DIAL_AUDIO_ONLINE)" />
+          <svg-icon
+            class="tool-icon"
+            name="yyth"
+            @click="startAudioOnline(Constant.DIAL_AUDIO_ONLINE)"
+          />
           <div>语音通话</div>
         </div>
         <div class="tool-btn">
-          <svg-icon class="tool-icon" name="spth" @click="startAudioOnline(Constant.DIAL_VIDEO_ONLINE)"  />
+          <svg-icon
+            class="tool-icon"
+            name="spth"
+            @click="startAudioOnline(Constant.DIAL_VIDEO_ONLINE)"
+          />
           <div>视频通话</div>
         </div>
         <div class="tool-btn">
@@ -167,23 +182,11 @@
         </div>
       </div>
     </div>
-    <!-- 来电弹窗 -->
-    <!-- <div v-if="rtcStore.imSate.videoCallModal && !inCall" class="call-modal">
-      <p>{{ rtcStore.imSate.callName }} 正在呼叫你</p>
-      <button @click="acceptCall">接听</button>
-      <button @click="rejectCall">拒接</button>
-    </div> -->
-
-    <!-- 通话中显示挂断按钮 -->
-    <!-- <div v-if="inCall" class="call-modal">
-      <p>与 {{ rtcStore.imSate.callName }} 通话中...</p>
-      <button @click="hangupCall">挂断</button>
-    </div> -->
+
   </div>
 </template>
 
 <script setup>
-import { ref, computed, onMounted, onUnmounted, onBeforeUnmount } from "vue";
 import { useRouter, useRoute } from "vue-router";
 import { useWebSocketStore } from "@/stores/modules/webSocketStore.js";
 import { useWalletStore } from "@/stores/modules/walletStore.js";
@@ -239,11 +242,6 @@ watch(
 // 平台判断
 const isMobile = Capacitor.getPlatform() !== "web";
 
-// 语音
-const isTouchDevice = ref(false);
-const mediaRecorder = ref(null); // 录音对象
-const audioChunks = ref([]); // 录音数据
-
 // 计算当前底部总高度
 const currentBottomHeight = computed(() => {
   if (keyboardHeight.value > 0) return keyboardHeight.value;
@@ -300,90 +298,84 @@ const setupKeyboardListeners = async () => {
   });
 };
 
-// 录音
-const startAudio = async (event) => {
-  if (event.type === "touchstart") {
-    isTouchDevice.value = true;
-  }
+// 录音相关状态
+const voiceMode = ref(false); // false: 文字输入, true: 语音模式
+const recording = ref(false);
+const cancelRecording = ref(false);
+const startY = ref(0);
+let mediaRecorder = null;
+let audioChunks = [];
+
+// 切换文字/语音输入模式
+const toggleVoiceMode = () => {
+  voiceMode.value = !voiceMode.value;
+  // 切换时关闭表情和工具面板,隐藏键盘
+  showEmoji.value = false;
+  showTools.value = false;
+  if (isMobile) Keyboard.hide();
+  keyboardHeight.value = 0;
+  scrollToBottom();
+};
+
+// 录音事件
+const handleTouchStart = async (e) => {
+  startY.value = e.touches[0].clientY;
+  cancelRecording.value = false;
+  recording.value = true;
+  audioChunks = [];
 
-  // 如果是触摸设备且事件是鼠标事件,则忽略
-  if (isTouchDevice.value && event.type === "mousedown") {
-    return;
-  }
   try {
-    // 请求麦克风权限
     const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
-
-    // 创建 MediaRecorder 实例
-    mediaRecorder.value = new MediaRecorder(stream, {
+    mediaRecorder = new MediaRecorder(stream, {
       mimeType: "audio/webm; codecs=opus",
     });
-
-    // 收集音频数据
-    mediaRecorder.value.ondataavailable = (e) => {
-      audioChunks.value.push(e.data);
+    mediaRecorder.ondataavailable = (ev) => {
+      audioChunks.push(ev.data);
     };
-
-    mediaRecorder.value.start(1000); // 每1秒收集一次数据
-    console.log("Recording started");
-  } catch (error) {
-    console.error("Error accessing microphone:", error);
+    mediaRecorder.start(1000);
+    console.log("开始录音");
+  } catch (err) {
+    console.error("麦克风权限获取失败", err);
+    recording.value = false;
   }
 };
-// 停止录音
-const stopRecording = async () => {
-  return new Promise(async (resolve) => {
-    if (!mediaRecorder.value) {
-      resolve(new Uint8Array());
-      return;
-    }
-    // 停止录音
-    mediaRecorder.value.stop();
-    mediaRecorder.value.stream.getTracks().forEach((track) => track.stop());
-
-    // 等待最后的数据可用
-    mediaRecorder.value.onstop = async () => {
-      // 合并所有音频片段
-      const audioBlob = new Blob(audioChunks.value, { type: "audio/webm" });
-      // 转换为 Uint8Array
-      const arrayBuffer = await audioBlob.arrayBuffer();
-      const audioData = new Uint8Array(arrayBuffer);
-
-      resolve(audioData);
-    };
-  });
-};
 
-// 发送音频消息
-const sendAudioMessage = async (event) => {
-  if (isTouchDevice.value && event.type === "mouseup") {
-    return;
+const handleTouchMove = (e) => {
+  const currentY = e.touches[0].clientY;
+  if (startY.value - currentY > 50) {
+    cancelRecording.value = true;
+  } else {
+    cancelRecording.value = false;
   }
-  console.log("发送音频消息");
-  try {
-    // 1. 停止录音并获取音频数据
-    const audioData = await stopRecording();
-
-    // 2. 准备消息体
-    const message = {
-      content: text.value, // 如果有文本内容
-      contentType: MSG_TYPE.AUDIO, // 音频消息类型
-      messageType: MESSAGE_TYPE_USER, // 单聊消息
-
-      fileSuffix: "wav", // 使用webm后缀更准确
-      file: audioData, // 将Uint8Array转为普通数组
-    };
+};
 
-    // 3. 通过WebSocket发送
-    wsStore.sendMessage(message);
+const handleTouchEnd = () => {
+  if (!mediaRecorder) return;
+  mediaRecorder.stop();
+  mediaRecorder.stream.getTracks().forEach((t) => t.stop());
+  recording.value = false;
 
-    // 4. 重置状态
-    mediaRecorder.value = null;
-    audioChunks.value = [];
-  } catch (error) {
-    console.error("Error sending audio message:", error);
-  }
+  mediaRecorder.onstop = async () => {
+    if (cancelRecording.value) {
+      console.log("录音取消");
+      return;
+    }
+    if (audioChunks.length === 0) return;
+    const audioBlob = new Blob(audioChunks, { type: "audio/webm" });
+    const arrayBuffer = await audioBlob.arrayBuffer();
+    const audioData = new Uint8Array(arrayBuffer);
+
+    wsStore.sendMessage({
+      content: "",
+      contentType: MSG_TYPE.AUDIO,
+      messageType: MESSAGE_TYPE_USER,
+      fileSuffix: "webm",
+      file: audioData,
+    });
+    console.log("语音已发送");
+  };
 };
+
 // 发送消息
 const sendMessage = () => {
   if (!text.value.trim()) return;
@@ -740,4 +732,37 @@ const goDetail = () => router.push("detail");
   background: black;
   object-fit: cover;
 }
+.hold-talk-btn {
+  flex: 1;
+  text-align: center;
+  background: #f5f5f5;
+  padding: 6px 16px;
+  border-radius: 17px;
+  color: #666;
+  font-weight: 500;
+  font-size: 15px;
+  user-select: none;
+  -webkit-user-select: none;
+  -webkit-touch-callout: none;
+  margin: 0 12px;
+  line-height: 24px;
+  border: 1px solid #f5f5f5;
+}
+.recording-toast {
+  position: fixed;
+  bottom: 120px;
+  left: 50%;
+  transform: translateX(-50%);
+  padding: 12px 20px;
+  background: rgba(0, 0, 0, 0.75);
+  color: #fff;
+  border-radius: 8px;
+  font-size: 14px;
+  user-select: none;
+  -webkit-user-select: none;
+  -webkit-touch-callout: none;
+}
+.cancel-msg {
+  color: #ff4d4f;
+}
 </style>

+ 2 - 0
src/views/im/contactList/invitation/index.vue

@@ -83,6 +83,8 @@ const changeFun = async (val) => {
     if(res.code == 200){
         let text = val == 2?'添加成功':'已拒绝'
         showToast(text);
+        list.value = [];
+        getfriendRequest();
     }
     showAgree.value = false;
 }