|
@@ -1,5 +1,8 @@
|
|
|
<template>
|
|
|
- <div class="container" :style="{ height: `calc(100% - ${currentBottomHeight}px)` }">
|
|
|
+ <div
|
|
|
+ class="container"
|
|
|
+ :style="{ height: `calc(100% - ${currentBottomHeight}px)` }"
|
|
|
+ >
|
|
|
<div class="chat-bg"></div>
|
|
|
<!-- 顶部导航 -->
|
|
|
<div class="header-chat">
|
|
@@ -11,9 +14,16 @@
|
|
|
<!-- 聊天消息区域 -->
|
|
|
<div class="chat-list" ref="chatListRef">
|
|
|
<div v-for="(item, index) in wsStore.messages" :key="index">
|
|
|
- <div class="chat-time">{{ formatTime(item.timestamp || Date.now()) }}</div>
|
|
|
+ <div class="chat-time">
|
|
|
+ {{ formatTime(item.timestamp || Date.now()) }}
|
|
|
+ </div>
|
|
|
<div class="box">
|
|
|
- <div class="list-item" :class="walletStore.account == item.toUsername ?'' : 'flex-reverse'">
|
|
|
+ <div
|
|
|
+ class="list-item"
|
|
|
+ :class="
|
|
|
+ walletStore.account == item.toUsername ? '' : 'flex-reverse'
|
|
|
+ "
|
|
|
+ >
|
|
|
<!-- 头像 -->
|
|
|
<van-image
|
|
|
class="list-img"
|
|
@@ -24,8 +34,10 @@
|
|
|
/>
|
|
|
<!-- 内容 -->
|
|
|
<div class="list-cont">
|
|
|
- <div>{{ item.fromUsername || '匿名用户' }}</div>
|
|
|
- <div class="content" v-if="item.contentType === 1">{{ item.content }}</div>
|
|
|
+ <div>{{ item.fromUsername || "匿名用户" }}</div>
|
|
|
+ <div class="content" v-if="item.contentType === 1">
|
|
|
+ {{ item.content }}
|
|
|
+ </div>
|
|
|
<!-- 可扩展 contentType === 2 图片,3 名片等 -->
|
|
|
</div>
|
|
|
</div>
|
|
@@ -36,7 +48,15 @@
|
|
|
<!-- 输入框 -->
|
|
|
<div class="page-foot">
|
|
|
<div class="flex-box">
|
|
|
- <svg-icon class="page-icon" name="voice" v-longpress="pressEvents" />
|
|
|
+ <svg-icon
|
|
|
+ type="button"
|
|
|
+ class="page-icon"
|
|
|
+ name="voice"
|
|
|
+ @mousedown="startAudio"
|
|
|
+ @touchstart="startAudio"
|
|
|
+ @mouseup="sendAudioMessage"
|
|
|
+ @touchend="sendAudioMessage"
|
|
|
+ />
|
|
|
<van-field
|
|
|
rows="1"
|
|
|
type="textarea"
|
|
@@ -48,32 +68,62 @@
|
|
|
placeholder="输入文本"
|
|
|
@keyup.enter="sendMessage"
|
|
|
/>
|
|
|
- <svg-icon class="page-icon mr12 emoji-toggle" name="emoji" @click="toggleAppBox(1)" />
|
|
|
- <svg-icon class="page-icon tools-toggle" name="add2" @click="toggleAppBox(2)" />
|
|
|
+ <svg-icon
|
|
|
+ class="page-icon mr12 emoji-toggle"
|
|
|
+ name="emoji"
|
|
|
+ @click="toggleAppBox(1)"
|
|
|
+ />
|
|
|
+ <svg-icon
|
|
|
+ class="page-icon tools-toggle"
|
|
|
+ name="add2"
|
|
|
+ @click="toggleAppBox(2)"
|
|
|
+ />
|
|
|
</div>
|
|
|
|
|
|
<!-- 表情面板 -->
|
|
|
- <div class="app-box" v-show="showEmoji" :style="{ height: `${appBoxHeight}px` }" ref="emojiRef">
|
|
|
+ <div
|
|
|
+ class="app-box"
|
|
|
+ v-show="showEmoji"
|
|
|
+ :style="{ height: `${appBoxHeight}px` }"
|
|
|
+ ref="emojiRef"
|
|
|
+ >
|
|
|
😀 😃 😄 😁 😆 😅 😂 🤣 😊 😇 🙂 🙃 😉 😌 😍
|
|
|
</div>
|
|
|
|
|
|
<!-- 工具栏面板 -->
|
|
|
- <div class="app-box" v-show="showTools" :style="{ height: `${appBoxHeight}px` }" ref="toolsRef">
|
|
|
+ <div
|
|
|
+ class="app-box"
|
|
|
+ v-show="showTools"
|
|
|
+ :style="{ height: `${appBoxHeight}px` }"
|
|
|
+ ref="toolsRef"
|
|
|
+ >
|
|
|
<div class="tool-btn">
|
|
|
<svg-icon class="tool-icon" name="tp" />
|
|
|
<div>图片</div>
|
|
|
</div>
|
|
|
- <div class="tool-btn"><svg-icon class="tool-icon" name="ps" /><div>拍摄</div></div>
|
|
|
- <div class="tool-btn"><svg-icon class="tool-icon" name="yyth" /><div>语音通话</div></div>
|
|
|
- <div class="tool-btn"><svg-icon class="tool-icon" name="spth" /><div>视频通话</div></div>
|
|
|
- <div class="tool-btn"><svg-icon class="tool-icon" name="mp" /><div>名片</div></div>
|
|
|
+ <div class="tool-btn">
|
|
|
+ <svg-icon class="tool-icon" name="ps" />
|
|
|
+ <div>拍摄</div>
|
|
|
+ </div>
|
|
|
+ <div class="tool-btn">
|
|
|
+ <svg-icon class="tool-icon" name="yyth" />
|
|
|
+ <div>语音通话</div>
|
|
|
+ </div>
|
|
|
+ <div class="tool-btn">
|
|
|
+ <svg-icon class="tool-icon" name="spth" />
|
|
|
+ <div>视频通话</div>
|
|
|
+ </div>
|
|
|
+ <div class="tool-btn">
|
|
|
+ <svg-icon class="tool-icon" name="mp" />
|
|
|
+ <div>名片</div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { ref, computed, onMounted, onUnmounted,onBeforeUnmount } from "vue";
|
|
|
+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";
|
|
@@ -114,8 +164,9 @@ const isMobile = Capacitor.getPlatform() !== "web";
|
|
|
|
|
|
// 底部高度动态计算
|
|
|
// 语音
|
|
|
-const mediaRecorder = ref(null); // 录音对象
|
|
|
-const audioChunks = ref([]) // 录音数据
|
|
|
+const isTouchDevice = ref(false);
|
|
|
+const mediaRecorder = ref(null); // 录音对象
|
|
|
+const audioChunks = ref([]); // 录音数据
|
|
|
|
|
|
// 计算当前底部总高度
|
|
|
const currentBottomHeight = computed(() => {
|
|
@@ -158,25 +209,35 @@ const setupKeyboardListeners = async () => {
|
|
|
};
|
|
|
|
|
|
// 录音
|
|
|
-const startAudio = async () => {
|
|
|
- try {
|
|
|
+const startAudio = async (event) => {
|
|
|
+ if (event.type === "touchstart") {
|
|
|
+ isTouchDevice.value = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果是触摸设备且事件是鼠标事件,则忽略
|
|
|
+ if (isTouchDevice.value && event.type === "mousedown") {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ try {
|
|
|
// 请求麦克风权限
|
|
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
|
|
|
|
// 创建 MediaRecorder 实例
|
|
|
- mediaRecorder.value = new MediaRecorder(stream, { mimeType: 'audio/webm; codecs=opus' });
|
|
|
-
|
|
|
+ mediaRecorder.value = new MediaRecorder(stream, {
|
|
|
+ mimeType: "audio/webm; codecs=opus",
|
|
|
+ });
|
|
|
+
|
|
|
// 收集音频数据
|
|
|
mediaRecorder.value.ondataavailable = (e) => {
|
|
|
audioChunks.value.push(e.data);
|
|
|
};
|
|
|
-
|
|
|
+
|
|
|
mediaRecorder.value.start(1000); // 每1秒收集一次数据
|
|
|
- console.log('Recording started');
|
|
|
+ console.log("Recording started");
|
|
|
} catch (error) {
|
|
|
- console.error('Error accessing microphone:', error);
|
|
|
+ console.error("Error accessing microphone:", error);
|
|
|
}
|
|
|
-}
|
|
|
+};
|
|
|
// 停止录音
|
|
|
const stopRecording = async () => {
|
|
|
return new Promise(async (resolve) => {
|
|
@@ -184,67 +245,63 @@ const stopRecording = async () => {
|
|
|
resolve(new Uint8Array());
|
|
|
return;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 停止录音
|
|
|
mediaRecorder.value.stop();
|
|
|
- mediaRecorder.value.stream.getTracks().forEach(track => track.stop());
|
|
|
-
|
|
|
+ mediaRecorder.value.stream.getTracks().forEach((track) => track.stop());
|
|
|
+
|
|
|
// 等待最后的数据可用
|
|
|
mediaRecorder.value.onstop = async () => {
|
|
|
// 合并所有音频片段
|
|
|
- const audioBlob = new Blob(audioChunks.value, { type: 'audio/webm' });
|
|
|
-
|
|
|
+ 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 () => {
|
|
|
+const sendAudioMessage = async (event) => {
|
|
|
+ if (isTouchDevice.value && event.type === "mouseup") {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ console.log("发送音频消息");
|
|
|
try {
|
|
|
// 1. 停止录音并获取音频数据
|
|
|
const audioData = await stopRecording();
|
|
|
-
|
|
|
+
|
|
|
// 2. 准备消息体
|
|
|
const message = {
|
|
|
content: text.value, // 如果有文本内容
|
|
|
- contentType: MSG_TYPE.AUDIO, // 音频消息类型
|
|
|
+ contentType: MSG_TYPE.AUDIO, // 音频消息类型
|
|
|
messageType: MESSAGE_TYPE_USER, // 单聊消息
|
|
|
to: route.query.uuid, // 接收方ID
|
|
|
fileSuffix: "webm", // 使用webm后缀更准确
|
|
|
- file: Array.from(audioData) // 将Uint8Array转为普通数组
|
|
|
+ file: Array.from(audioData), // 将Uint8Array转为普通数组
|
|
|
};
|
|
|
-
|
|
|
+
|
|
|
// 3. 通过WebSocket发送
|
|
|
wsStore.sendMessage(message);
|
|
|
-
|
|
|
+
|
|
|
// 4. 重置状态
|
|
|
mediaRecorder.value = null;
|
|
|
audioChunks.value = [];
|
|
|
-
|
|
|
} catch (error) {
|
|
|
- console.error('Error sending audio message:', error);
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-const pressEvents = (event, type) => {
|
|
|
- if (type === 'longpress') {
|
|
|
- console.log('长按触发');
|
|
|
- } else {
|
|
|
- console.log('点击/放开触发');
|
|
|
+ console.error("Error sending audio message:", error);
|
|
|
}
|
|
|
};
|
|
|
+
|
|
|
// 发送消息
|
|
|
const sendMessage = () => {
|
|
|
if (!text.value.trim()) return;
|
|
|
const message = {
|
|
|
content: text.value,
|
|
|
- contentType: MSG_TYPE.TEXT, // 1: 文本消息
|
|
|
+ contentType: MSG_TYPE.TEXT, // 1: 文本消息
|
|
|
messageType: MESSAGE_TYPE_USER, // 1: 单聊天
|
|
|
- to:route.query.uuid,
|
|
|
+ to: route.query.uuid,
|
|
|
};
|
|
|
wsStore.sendMessage(message);
|
|
|
text.value = "";
|
|
@@ -267,14 +324,14 @@ onMounted(() => {
|
|
|
friendUsername: route.query.uuid,
|
|
|
});
|
|
|
scrollToBottom();
|
|
|
- document.addEventListener('click', handleClickOutside);
|
|
|
+ document.addEventListener("click", handleClickOutside);
|
|
|
});
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
if (isMobile) Keyboard.removeAllListeners();
|
|
|
});
|
|
|
onBeforeUnmount(() => {
|
|
|
- document.removeEventListener('click', handleClickOutside);
|
|
|
+ document.removeEventListener("click", handleClickOutside);
|
|
|
});
|
|
|
|
|
|
// 判断是否点击在元素外
|
|
@@ -287,7 +344,7 @@ const handleClickOutside = (event) => {
|
|
|
showEmoji.value &&
|
|
|
emojiEl &&
|
|
|
!emojiEl.contains(target) &&
|
|
|
- !target.closest('.emoji-toggle')
|
|
|
+ !target.closest(".emoji-toggle")
|
|
|
) {
|
|
|
showEmoji.value = false;
|
|
|
}
|
|
@@ -296,7 +353,7 @@ const handleClickOutside = (event) => {
|
|
|
showTools.value &&
|
|
|
toolsEl &&
|
|
|
!toolsEl.contains(target) &&
|
|
|
- !target.closest('.tools-toggle')
|
|
|
+ !target.closest(".tools-toggle")
|
|
|
) {
|
|
|
showTools.value = false;
|
|
|
}
|
|
@@ -508,16 +565,18 @@ const goDetail = () => router.push("detail");
|
|
|
&.visible {
|
|
|
transform: translateY(0);
|
|
|
}
|
|
|
- .tool-btn{
|
|
|
+ .tool-btn {
|
|
|
margin-right: 32px;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
align-items: center;
|
|
|
- font-family: PingFang SC, PingFang SC;
|
|
|
+ font-family:
|
|
|
+ PingFang SC,
|
|
|
+ PingFang SC;
|
|
|
font-weight: 400;
|
|
|
font-size: 12px;
|
|
|
color: #000000;
|
|
|
- .tool-icon{
|
|
|
+ .tool-icon {
|
|
|
width: 56px;
|
|
|
height: 56px;
|
|
|
margin-bottom: 4px;
|