|
@@ -7,7 +7,7 @@
|
|
<!-- 顶部导航 -->
|
|
<!-- 顶部导航 -->
|
|
<div class="header-chat">
|
|
<div class="header-chat">
|
|
<svg-icon class="page-icon" name="lf-arrow" @click="goBack" />
|
|
<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" />
|
|
<svg-icon class="page-icon" name="more" @click="goDetail" />
|
|
</div>
|
|
</div>
|
|
|
|
|
|
@@ -65,18 +65,6 @@
|
|
class="audio-message"
|
|
class="audio-message"
|
|
v-else-if="item.contentType === MSG_TYPE.AUDIO"
|
|
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
|
|
<messageAudio
|
|
:src="item?.localUrl || IM_PATH + item.url"
|
|
:src="item?.localUrl || IM_PATH + item.url"
|
|
:isSender="isSender(item.toUsername)"
|
|
:isSender="isSender(item.toUsername)"
|
|
@@ -94,26 +82,39 @@
|
|
<!-- 输入框 -->
|
|
<!-- 输入框 -->
|
|
<div class="page-foot">
|
|
<div class="page-foot">
|
|
<div class="flex-box">
|
|
<div class="flex-box">
|
|
|
|
+ <!-- 录音/文字切换按钮 -->
|
|
<svg-icon
|
|
<svg-icon
|
|
type="button"
|
|
type="button"
|
|
class="page-icon"
|
|
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
|
|
<svg-icon
|
|
class="page-icon mr12 emoji-toggle"
|
|
class="page-icon mr12 emoji-toggle"
|
|
name="emoji"
|
|
name="emoji"
|
|
@@ -126,6 +127,12 @@
|
|
/>
|
|
/>
|
|
</div>
|
|
</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
|
|
<div
|
|
class="app-box"
|
|
class="app-box"
|
|
@@ -154,11 +161,19 @@
|
|
<div>拍摄</div>
|
|
<div>拍摄</div>
|
|
</div>
|
|
</div>
|
|
<div class="tool-btn">
|
|
<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>
|
|
</div>
|
|
</div>
|
|
<div class="tool-btn">
|
|
<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>
|
|
</div>
|
|
</div>
|
|
<div class="tool-btn">
|
|
<div class="tool-btn">
|
|
@@ -167,23 +182,11 @@
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</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>
|
|
</div>
|
|
</template>
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
<script setup>
|
|
-import { ref, computed, onMounted, onUnmounted, onBeforeUnmount } from "vue";
|
|
|
|
import { useRouter, useRoute } from "vue-router";
|
|
import { useRouter, useRoute } from "vue-router";
|
|
import { useWebSocketStore } from "@/stores/modules/webSocketStore.js";
|
|
import { useWebSocketStore } from "@/stores/modules/webSocketStore.js";
|
|
import { useWalletStore } from "@/stores/modules/walletStore.js";
|
|
import { useWalletStore } from "@/stores/modules/walletStore.js";
|
|
@@ -239,11 +242,6 @@ watch(
|
|
// 平台判断
|
|
// 平台判断
|
|
const isMobile = Capacitor.getPlatform() !== "web";
|
|
const isMobile = Capacitor.getPlatform() !== "web";
|
|
|
|
|
|
-// 语音
|
|
|
|
-const isTouchDevice = ref(false);
|
|
|
|
-const mediaRecorder = ref(null); // 录音对象
|
|
|
|
-const audioChunks = ref([]); // 录音数据
|
|
|
|
-
|
|
|
|
// 计算当前底部总高度
|
|
// 计算当前底部总高度
|
|
const currentBottomHeight = computed(() => {
|
|
const currentBottomHeight = computed(() => {
|
|
if (keyboardHeight.value > 0) return keyboardHeight.value;
|
|
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 {
|
|
try {
|
|
- // 请求麦克风权限
|
|
|
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
-
|
|
|
|
- // 创建 MediaRecorder 实例
|
|
|
|
- mediaRecorder.value = new MediaRecorder(stream, {
|
|
|
|
|
|
+ mediaRecorder = new MediaRecorder(stream, {
|
|
mimeType: "audio/webm; codecs=opus",
|
|
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 = () => {
|
|
const sendMessage = () => {
|
|
if (!text.value.trim()) return;
|
|
if (!text.value.trim()) return;
|
|
@@ -740,4 +732,37 @@ const goDetail = () => router.push("detail");
|
|
background: black;
|
|
background: black;
|
|
object-fit: cover;
|
|
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>
|
|
</style>
|