wkw преди 1 месец
родител
ревизия
37709d5501
променени са 4 файла, в които са добавени 191 реда и са изтрити 142 реда
  1. 3 1
      src/common/constant/msgType.js
  2. 87 86
      src/views/im/chat/components/AtUserList/index.vue
  3. 58 11
      src/views/im/chat/index.vue
  4. 43 44
      src/views/im/hook/messagesHook.js

+ 3 - 1
src/common/constant/msgType.js

@@ -24,9 +24,10 @@ export const MESSAGE_TYPE_GROUP = 2; // 群聊
  
 export const MESSAGE_TRANS_TYPE = "webrtc";
 
-export const MESSAGE_REVOKE = 101; // 撤回消息
+export const MESSAGE_REVOKE = 101; // 个人撤回消息
 export const MESSAGE_RECEIPT = 102; // 消息已确认
 
+
 // 好友
 export const MESSAGE_APPLY_FRIEND = 111; //申请好友
 export const MESSAGE_PASS_FRIEND = 112;  //同意
@@ -40,6 +41,7 @@ export const EXIT_GROUP = 212;  //退出群
 export const REMOVE_GROUP = 213;  //移出群
 export const DELETE_GROUP = 214;  //解散群
 export const CREATE_GROUP = 215;  //创建群
+export const MESSAGE_REVOKE_GROUP = 219; // 群消息撤回
 
 export const MSG_TYPE_GROUP = [
   // JION_GROUP,

+ 87 - 86
src/views/im/chat/components/AtUserList/index.vue

@@ -1,95 +1,96 @@
 <template>
     <!-- 底部弹出 -->
-    <van-popup
-      v-model:show="show"
-      position="bottom"
-      round
-      style="height: 60%;"
-    >
-      <!-- 搜索栏 -->
-      <div class="p-3">
-        <van-search
-          v-model="keyword"
-          placeholder="搜索用户"
-          show-action
-          @cancel="close"
-        />
-      </div>
-  
-      <!-- 用户列表 -->
-      <van-list
-        v-model:loading="loading"
-        :finished="finished"
-        finished-text="没有更多了"
-        @load="onLoad"
-      >
-        <van-cell
-          v-for="user in filteredUsers"
-          :key="user.id"
-          :title="user.name"
-          :label="user.desc"
-          clickable
-          @click="selectUser(user)"
-        >
-          <template #icon>
-            <van-image
-              round
-              width="40px"
-              height="40px"
-              :src="user.avatar"
+    <van-popup v-model:show="show" position="bottom" round style="height: 60%">
+        <!-- 搜索栏 -->
+        <div class="p-3">
+            <van-search
+                v-model="keyword"
+                placeholder="搜索用户"
+                show-action
+                @cancel="close"
             />
-          </template>
-        </van-cell>
-      </van-list>
+        </div>
+
+        <!-- 用户列表 -->
+        <van-list>
+            <van-cell
+                v-for="user in filteredUsers"
+                :key="user.id"
+                :title="user.nickname"
+                clickable
+                @click="selectUser(user)"
+            >
+                <template #icon>
+                    <van-image
+                        round
+                        width="40px"
+                        height="40px"
+                        :src="IM_PATH + user.avatar"
+                    />
+                </template>
+            </van-cell>
+        </van-list>
     </van-popup>
-  </template>
-  
-  <script setup>
-  const emit = defineEmits(["select"]);
-  
-  const show = ref(false);
-  const keyword = ref("");
-  const loading = ref(false);
-  const finished = ref(false);
-  
-  // 模拟用户数据
-  const users = ref([
-    { id: 1, name: "小明", desc: "前端开发", avatar: "https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg" },
-    { id: 2, name: "小红", desc: "产品经理", avatar: "https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg" },
-    { id: 3, name: "小李", desc: "后端工程师", avatar: "https://fastly.jsdelivr.net/npm/@vant/assets/cat.jpeg" },
-  ]);
-  
-  // 关键字过滤
-  const filteredUsers = computed(() => {
+</template>
+
+<script setup>
+import { useWebSocketStore } from '@/stores/modules/webSocketStore.js';
+import { useWalletStore } from "@/stores/modules/walletStore.js";
+import { groupList } from '@/api/path/im.api';
+const wsStore = useWebSocketStore();
+const walletStore = useWalletStore();
+const IM_PATH = import.meta.env.VITE_IM_PATH_FIlE;
+const emit = defineEmits(['select']);
+
+const show = ref(false);
+const keyword = ref('');
+const loading = ref(false);
+const finished = ref(false);
+// 模拟用户数据
+const users = ref([]);
+
+// 获取群成员信息
+const fetchGroupMembers = async () => {
+    try {
+        const res = await groupList(wsStore.toUserInfo.uuid);
+
+        users.value = res.data || [];
+    } catch (e) {
+        console.error('获取群成员失败', e);
+    }
+};
+
+// 关键字过滤
+const filteredUsers = computed(() => {
     if (!keyword.value) return users.value;
     return users.value.filter(u => u.name.includes(keyword.value));
-  });
-  
-  // 模拟加载更多
-  const onLoad = () => {
-    setTimeout(() => {
-      finished.value = true;
-      loading.value = false;
-    }, 1000);
-  };
-  
-  // 选中用户
-  const selectUser = (user) => {
-    console.log("选择用户:", user);
-    emit("select", user); // ✅ 通知父组件
+});
+
+// 模拟加载更多
+// const onLoad = () => {
+//     setTimeout(() => {
+//         finished.value = true;
+//         loading.value = false;
+//     }, 1000);
+// };
+
+// 选中用户
+const selectUser = user => {
+    console.log('选择用户:', user);
+    emit('select', user); // ✅ 通知父组件
     close();
-  };
-  
-  // 关闭弹窗
-  const close = () => {
+};
+
+// 关闭弹窗
+const close = () => {
     show.value = false;
-  };
-  
-  // 供父组件调用打开
-  const open = () => {
+};
+
+// 供父组件调用打开
+const open = () => {
     show.value = true;
-  };
-  
-  defineExpose({ open });
-  </script>
-  
+    fetchGroupMembers();
+};
+
+defineExpose({ open });
+</script>

+ 58 - 11
src/views/im/chat/index.vue

@@ -12,7 +12,7 @@
     </div>
 
     <!-- 群公告 -->
-    <div class="groupNotice">
+    <div class="groupNotice" v-if="wsStore.toUserInfo.type == 'group'">
       <div class="m-ellipsis">{{ wsStore.toUserInfo.notice }}</div>
       <svg-icon class="item-icon" name="right1" />
     </div>
@@ -43,7 +43,7 @@
               <div>{{ item.sender?item.sender.nickname: (item.nickname || item.fromUsername || "匿名用户") }}</div>
 
               <!-- 文本消息 -->
-              <div class="content" v-if="item.contentType === MSG_TYPE.TEXT">
+              <div class="content" v-if="item.contentType === MSG_TYPE.TEXT" @click="onLongPress(item)">
                 {{ item.content }}
               </div>
 
@@ -94,6 +94,13 @@
 
     <!-- @页面 -->
     <AtUserList ref="atList" @select="onSelectUser" />
+    <!-- 撤回弹窗 -->
+    <van-action-sheet
+      v-model:show="showActionSheet"
+      :actions="actions"
+      cancel-text="取消"
+      @select="onActionSelect"
+    />
 
     <!-- 输入框 -->
     <div class="page-foot">
@@ -224,7 +231,7 @@ import { useWebSocketStore } from "@/stores/modules/webSocketStore.js";
 import { useWalletStore } from "@/stores/modules/walletStore.js";
 import { Keyboard } from "@capacitor/keyboard";
 import { Capacitor } from "@capacitor/core";
-import { MSG_TYPE, MESSAGE_TYPE_USER,MESSAGE_TYPE_GROUP } from "@/common/constant/msgType";
+import { MSG_TYPE, MESSAGE_TYPE_USER,MESSAGE_TYPE_GROUP,MESSAGE_REVOKE,MESSAGE_REVOKE_GROUP } from "@/common/constant/msgType";
 import messageAudio from "@/views/im/components/messageAudio/index.vue";
 import { showToast, showImagePreview } from "vant";
 import { useWebRTCStore } from "@/stores/modules/webrtcStore";
@@ -261,7 +268,7 @@ const emojis = [
 const atList = ref();
 
 const isSender = (toUsername, item) => {
-  if(item.sender){
+  if(item?.sender){
     return walletStore.account !== item.sender.uuid;
   }
   return walletStore.account === toUsername;
@@ -292,7 +299,6 @@ const scrollToBottom = () => {
 watch(
   () => wsStore.indexs,
   (newVal,oldVal) => {
-    // console.log(newVal, oldVal, 'scrollTo')
     if (newVal !== oldVal) {
       scrollToBottom();
     }
@@ -456,6 +462,7 @@ const handleTouchEnd = () => {
 
 // 监听输入 @
 const handleKeyPress = (e) => {
+  if(wsStore.toUserInfo.type == 'user') return;
   if (e.key === "@") {
     atList.value.open(); // 打开选人弹窗
   }
@@ -463,8 +470,38 @@ const handleKeyPress = (e) => {
 
 // 选择用户回填输入框
 const onSelectUser = (user) => {
-  // 在光标位置插入 @昵称(可改成 user.id)
-  // text.value += user.name + " ";
+  text.value += `${user.nickname} `;
+};
+// 撤回
+const currentMsg = ref('');
+const showActionSheet = ref(false)
+const actions = [
+  { name: "复制", key: "copy" },
+  { name: "撤回", key: "revoke" },
+];
+const onLongPress = (msg) => {
+  currentMsg.value = msg;
+  showActionSheet.value = true;
+};
+const onActionSelect = (action) => {
+  if (!currentMsg.value) return;
+
+  if (action.key === "copy") {
+    navigator.clipboard.writeText(currentMsg.value.content);
+    showToast("已复制");
+  } else if (action.key === "revoke") {
+    const message = {
+      content: JSON.stringify({ 
+        content: '',
+        msgId: currentMsg.value.id + '',
+      }), 
+      contentType: MSG_TYPE.NOTICE, // 1: 文本消息
+      messageType: wsStore.toUserInfo.type == 'user'?MESSAGE_REVOKE:MESSAGE_REVOKE_GROUP,
+    };
+    wsStore.sendMessage(message);
+    currentMsg.value.revoked = true;
+  }
+  showActionSheet.value = false;
 };
 
 // 发送消息
@@ -629,7 +666,7 @@ const goDetail = () =>{
     router.push({
       path: 'personal',
       query:{
-        uuid:wsStore.toUserInfo.type == 'user'?wsStore.toUserInfo.uuid:item.uuid,
+        uuid:wsStore.toUserInfo.uuid,
         type:2
       }
     })
@@ -958,9 +995,7 @@ const goDetail = () =>{
   background-color: #fff;
   height: 40px;
   line-height: 40px;
-  font-family:
-    PingFang SC,
-    PingFang SC;
+  font-family: PingFang SC, PingFang SC;
   font-weight: 400;
   font-size: 12px;
   color: #000000;
@@ -981,4 +1016,16 @@ const goDetail = () =>{
     color: #969799;
   }
 }
+.revoked-msg {
+  color: #999;
+  font-size: 12px;
+  font-style: italic;
+  text-align: center;
+  margin: 5px 0;
+}
+.burn-tag {
+  font-size: 10px;
+  color: red;
+  margin-left: 6px;
+}
 </style>

+ 43 - 44
src/views/im/hook/messagesHook.js

@@ -6,6 +6,7 @@ import { useWebSocketStore } from "@/stores/modules/webSocketStore";
 import { useWalletStore } from "@/stores/modules/walletStore.js";
 
 const IM_PATH = import.meta.env.VITE_IM_PATH_FIlE;
+let msgType = [Constant.REJECT_AUDIO_ONLINE, Constant.CANCELL_AUDIO_ONLINE, Constant.MESSAGE_REVOKE, Constant.MESSAGE_REVOKE_GROUP]
 
 // 格式化扩展消息提取
 const formatMessageExt = (message, ext) => {
@@ -14,6 +15,7 @@ const formatMessageExt = (message, ext) => {
     message.content= ext.content || message.content;
     message.msgId= ext.msgId || null;
     message.cc = ext.cc || message.cc;
+    message.id = ext.id || message.id;
   }
   return message
 }
@@ -23,24 +25,12 @@ const formatMessageExt = (message, ext) => {
 export const setMessageHook = (message, state) => {
   const wsStore = useWebSocketStore();
   const walletStore = useWalletStore();
-  // console.log('发送消息',message)
+  console.log('发送消息',message)
 
   // 好友通过处理
   if (message.messageType == MsgType.MESSAGE_PASS_FRIEND) {
     wsStore.funRestoreDelAuth(message.to);
   }
-  // 群通知(群系统通知不需要发送处理)
-  // if (MsgType.MSG_TYPE_GROUP.indexOf(message.messageType) >= 0) {
-  //   state.messages.push({
-  //     ...message,
-  //     toUsername: message.to,
-  //     contentType: MsgType.MSG_TYPE.NOTICE,
-  //     messageType: MsgType.MESSAGE_TYPE_GROUP
-  //   });
-  //   wsStore.indexs +=1;
-  //   return;
-  // }
-
   // 尝试解析扩展消息JOSON
   let msg;
   try{
@@ -51,9 +41,11 @@ export const setMessageHook = (message, state) => {
   }catch(e){}
 
   // 处理消息撤回
-  if (message.messageType === MsgType.MESSAGE_REVOKE) { 
-
-    return;
+  if (message.messageType === MsgType.MESSAGE_REVOKE || message.messageType === MsgType.MESSAGE_REVOKE_GROUP) { 
+    wsStore.messages = wsStore.messages.filter(item => {
+      if (item.id+'' === msg.msgId+'') return false;
+      return true;
+    });
   }
 
   // 语音和视频挂断消息处理
@@ -85,12 +77,10 @@ export const setMessageHook = (message, state) => {
   }
   // 文本消息
   if (message.contentType == MsgType.MSG_TYPE.TEXT) {
-    state.messages.push({
+    state.messages.push(formatMessageExt({
       ...message,
-      toUsername: message.friendUsername,
-      content: msg?msg.content: message.content,
-      msgId: msg?msg.msgId: null,
-    });
+      toUsername: message.friendUsername
+    }, msg));
     wsStore.indexs +=1;
   }
   // 音频消息
@@ -98,31 +88,32 @@ export const setMessageHook = (message, state) => {
     const blob = new Blob([message.file], { type: message.fileSuffix });
     const url = URL.createObjectURL(blob);
 
-    state.messages.push({
+    state.messages.push(formatMessageExt({
       ...message,
-      content: msg?msg.content: message.content,
-      msgId: msg?msg.msgId: null,
+      // content: msg?msg.content: message.content,
+      // msgId: msg?msg.msgId: null,
       toUsername: message.to,
       localUrl: url,
-    });
+    }, msg));
     wsStore.indexs +=1;
   }
   // 图片
   if (message.contentType === MsgType.MSG_TYPE.IMAGE) {
     const blob = new Blob([message.file], { type: message.fileSuffix });
     const url = URL.createObjectURL(blob);
-    state.messages.push({
+    state.messages.push(formatMessageExt({
       ...message,
-      content: msg?msg.content: message.content,
-      msgId: msg?msg.msgId: null,
+      // content: msg?msg.content: message.content,
+      // msgId: msg?msg.msgId: null,
       toUsername: message.to,
       localUrl: url,
-    });
+    }, msg));
     wsStore.indexs +=1;
   }
 
+
   // 同步更新会话列表缓存
-  if ((MsgType.MSG_TYPE_MAP.indexOf(message.contentType) >= 0 && !message.type) || message.contentType == Constant.REJECT_AUDIO_ONLINE || message.contentType == Constant.CANCELL_AUDIO_ONLINE){
+  if ((MsgType.MSG_TYPE_MAP.indexOf(message.contentType) >= 0 && !message.type) || msgType.includes(message.contentType)){
     wsStore.updateSessionList(message, 0, msg);
   }
 };
@@ -138,6 +129,7 @@ export const handleMessageHook = async (payload, state) => {
   const wsStore = useWebSocketStore();
   const walletStore = useWalletStore();
   const message = {...payload};
+  if (!message.contentType && !message.messageType) return;
 
   // 尝试解析扩展消息JOSON
   let msg;
@@ -147,7 +139,14 @@ export const handleMessageHook = async (payload, state) => {
       msg =  ''
     }
   }catch(e){}
-  
+  // 处理消息撤回
+  if (message.messageType === MsgType.MESSAGE_REVOKE || message.messageType === MsgType.MESSAGE_REVOKE_GROUP) {
+    console.log(wsStore.messages)
+    wsStore.messages = wsStore.messages.filter(item => {
+      if (item.id + '' === msg.msgId + '') return false;
+      return true;
+    });
+  }
   // 处理消息已到达
   if (message.messageType === MsgType.MESSAGE_RECEIPT) { 
     console.log('receipt', message, msg);
@@ -167,7 +166,7 @@ export const handleMessageHook = async (payload, state) => {
             ? message.avatar
             : (IM_PATH + (message.avatar || ''));
   // 同步更新会话列表缓存
-  if ((MsgType.MSG_TYPE_MAP.indexOf(message.contentType) >= 0 && !message.type) || message.contentType == Constant.REJECT_AUDIO_ONLINE || message.contentType == Constant.CANCELL_AUDIO_ONLINE) {
+  if ((MsgType.MSG_TYPE_MAP.indexOf(message.contentType) >= 0 && !message.type) || msgType.includes(message.contentType)) {
     const avatar = message.avatar;
     // 兼容
     if (Array.isArray(state.toUserInfo.avatar)) {
@@ -229,12 +228,12 @@ export const handleMessageHook = async (payload, state) => {
   }
   // 文本消息
   if (message.contentType === MsgType.MSG_TYPE.TEXT) {
-    state.messages.push({
+    state.messages.push(formatMessageExt({
       ...message,
       toUsername: message.to,
-      content: msg?msg.content: message.content,
-      msgId: msg?msg.msgId: null,
-    });
+      // content: msg?msg.content: message.content,
+      // msgId: msg?msg.msgId: null,
+    }, msg));
     wsStore.indexs +=1;
     setupNotifications(1)
     return
@@ -246,13 +245,13 @@ export const handleMessageHook = async (payload, state) => {
     });
     // 生成可播放的 ObjectURL
     const audioUrl = URL.createObjectURL(audioBlob);
-    state.messages.push({
+    state.messages.push(formatMessageExt({
       ...message,
       file: audioUrl,
       toUsername: message.to,
-      content: msg?msg.content: message.content,
-      msgId: msg?msg.msgId: null,
-    });
+      // content: msg?msg.content: message.content,
+      // msgId: msg?msg.msgId: null,
+    }, msg));
     wsStore.indexs +=1;
     setupNotifications(1)
     return
@@ -262,13 +261,13 @@ export const handleMessageHook = async (payload, state) => {
   if (message.contentType === MsgType.MSG_TYPE.IMAGE) {
     const blob = new Blob([message.file], { type: message.fileSuffix });
     const url = URL.createObjectURL(blob);
-    state.messages.push({
+    state.messages.push(formatMessageExt({
       ...message,
       file: url,
       toUsername: message.to,
-      content: msg?msg.content: message.content,
-      msgId: msg?msg.msgId: null,
-    });
+      // content: msg?msg.content: message.content,
+      // msgId: msg?msg.msgId: null,
+    }, msg));
     wsStore.indexs +=1;
     setupNotifications(1)
     return