lxr 2 месяцев назад
Родитель
Сommit
22949576a3

+ 13 - 0
src/api/modules/headerNotice.ts

@@ -111,3 +111,16 @@ export const getMessageNoRead = (params: { receiverId: string }) => {
     loading: false
   });
 };
+
+/**
+ * 陌生人/未关注人消息未读数量(与商家端 getStrangerMessageNum 一致)
+ * GET /message/getStrangerMessageNum?receiverId=xxx(alien-store)
+ * 返回 { notReadCount, userName?, createdTime? } 或类似结构
+ */
+export const getStrangerMessageNum = (params: { receiverId: string }) => {
+  return httpApi.get<{ notReadCount?: number; userName?: string; createdTime?: string; [key: string]: any }>(
+    `/alienStore/message/getStrangerMessageNum`,
+    params,
+    { loading: false }
+  );
+};

+ 48 - 2
src/layouts/components/Header/components/NotificationBell.vue

@@ -6,20 +6,27 @@
       </el-icon>
     </el-badge>
     <el-dialog v-model="dialogVisible" width="880px" class="notification-dialog" destroy-on-close :show-close="true" align-center>
-      <NotificationDrawerContent v-if="dialogVisible" @close="handleDrawerClose" />
+      <NotificationDrawerContent v-if="dialogVisible" ref="drawerContentRef" @close="handleDrawerClose" />
     </el-dialog>
   </div>
 </template>
 
 <script setup lang="ts">
-import { ref, onMounted } from "vue";
+import { ref, onMounted, onBeforeUnmount } from "vue";
+import { useDebounceFn } from "@vueuse/core";
 import { Bell } from "@element-plus/icons-vue";
 import { getMessageNoRead } from "@/api/modules/headerNotice";
 import { localGet } from "@/utils";
+import { useWebSocketStore } from "@/stores/modules/websocket";
 import NotificationDrawerContent from "./NotificationDrawerContent.vue";
 
 const dialogVisible = ref(false);
 const unreadCount = ref(0);
+const drawerContentRef = ref<InstanceType<typeof NotificationDrawerContent> | null>(null);
+const socketStore = useWebSocketStore();
+let cleanMessageFn: (() => void) | null = null;
+
+const WS_BASE = (import.meta.env.VITE_WS_BASE || "ws://120.26.186.130:8000/alienStore/socket/").replace(/\/$/, "");
 
 /** 与商家端 tabbar getMessage 一致:获取未读数量 */
 async function getMessage() {
@@ -40,6 +47,37 @@ async function getMessage() {
   }
 }
 
+/** 收到 WebSocket 消息时刷新(与商家端 handleWebSocketMessage 一致,debounce 300ms) */
+const handleWebSocketMessage = useDebounceFn(() => {
+  getMessage();
+  if (dialogVisible.value && drawerContentRef.value?.refresh) {
+    drawerContentRef.value.refresh();
+  }
+}, 300);
+
+async function connectWebSocket() {
+  const phone = localGet("iphone") || localGet("geeker-user")?.userInfo?.phone;
+  if (!phone) return;
+
+  const wsUrl =
+    WS_BASE.startsWith("wss") || WS_BASE.startsWith("ws")
+      ? `${WS_BASE}/store_${phone}`
+      : WS_BASE.replace("https", "wss").replace("http", "ws") + `/store_${phone}`;
+
+  if (!socketStore.isConnected || socketStore.lastConnectedUrl !== wsUrl) {
+    await socketStore.connect(wsUrl);
+  }
+
+  if (cleanMessageFn) {
+    cleanMessageFn();
+    cleanMessageFn = null;
+  }
+
+  cleanMessageFn = socketStore.subscribe("message", () => {
+    handleWebSocketMessage();
+  });
+}
+
 function openDialog() {
   dialogVisible.value = true;
   getMessage();
@@ -52,6 +90,14 @@ function handleDrawerClose() {
 
 onMounted(() => {
   getMessage();
+  connectWebSocket();
+});
+
+onBeforeUnmount(() => {
+  if (cleanMessageFn) {
+    cleanMessageFn();
+    cleanMessageFn = null;
+  }
 });
 </script>
 

+ 120 - 21
src/layouts/components/Header/components/NotificationDrawerContent.vue

@@ -36,6 +36,7 @@
               <component :is="cat.icon" />
             </el-icon>
             <span class="side-text">{{ cat.title }}</span>
+            <el-badge v-if="cat.unread > 0" :value="handleReadCount(cat.unread)" class="side-badge" />
           </div>
         </template>
       </div>
@@ -134,6 +135,7 @@
               v-for="(item, index) in currentMessageList"
               :key="item.id + '_' + index"
               class="message-card message-card-clickable"
+              :class="{ unread: item.unread }"
               @click="handleMessageItemClick(item)"
             >
               <div class="message-avatar">
@@ -146,8 +148,16 @@
                   <span class="message-sender">{{ item.senderName }}</span>
                   <span class="message-date">{{ item.date }}</span>
                 </div>
-                <div class="message-content">
-                  {{ processContent(item) }}
+                <div class="message-content-row">
+                  <span class="message-content">{{ processContent(item) }}</span>
+                  <div class="message-status">
+                    <!-- 非免打扰:显示数字徽标 -->
+                    <span v-if="item.isNotDisturb !== '1' && (item.notReadCount ?? 0) > 0" class="message-count">
+                      {{ handleReadCount(item.notReadCount!) }}
+                    </span>
+                    <!-- 免打扰:显示小红点 -->
+                    <span v-else-if="item.isNotDisturb === '1' && (item.notReadCount ?? 0) > 0" class="message-dot" />
+                  </div>
                 </div>
               </div>
             </div>
@@ -175,8 +185,8 @@ import {
   getNoticeListForHeader,
   readNoticeById,
   getCountUnreadByType,
-  getNoFriendMessage,
-  getMessageList
+  getMessageList,
+  getStrangerMessageNum
 } from "@/api/modules/headerNotice";
 import type { NoFriendMessageItem } from "@/api/modules/headerNotice";
 
@@ -202,7 +212,13 @@ interface MessageItem {
   content: string;
   date: string;
   unread: boolean;
+  /** 未读数量,用于展示红点/数字 */
+  notReadCount?: number;
+  /** 免打扰:'0' 非免打扰显示数字,'1' 免打扰显示小红点(与商家端一致) */
+  isNotDisturb?: string;
   avatar?: string;
+  /** 发送方 phoneId,用于跳转聊天页 */
+  phoneId?: string;
   /** 发送方 id,用于跳转聊天页 */
   senderId?: string;
   /** 消息类型,用于 processContent 展示 */
@@ -300,17 +316,21 @@ function switchMessageCategory(key: string) {
   }
 }
 
-// 未关注人消息:/message/getNoFriendMessage,参数 receiverId
+// 未关注人消息:/message/getNoFriendMessage,参数 receiverId。映射与商家端 message-notices 一致
 function mapNoFriendToMessageItem(item: NoFriendMessageItem): MessageItem {
   const createdTime = item.createdTime ?? "";
   const dateStr = createdTime.includes(" ") ? createdTime.split(" ")[0].replace(/-/g, "/") : createdTime.replace(/-/g, "/");
+  const notReadCount = item.notReadCount ?? (item.isRead === 0 ? 1 : 0);
   return {
     id: String(item.id ?? ""),
     senderName: item.senderName ?? item.userName ?? "",
     content: item.content ?? "",
     date: dateStr,
     unread: item.isRead === 0,
+    notReadCount,
+    isNotDisturb: (item as any).isNotDisturb ?? "0",
     avatar: item.userImage ?? item.senderImg ?? item.storeImg ?? undefined,
+    phoneId: (item as any).phoneId != null ? String((item as any).phoneId) : undefined,
     senderId: item.senderId != null ? String(item.senderId) : undefined,
     type: item.type,
     voiceDuration: (item as any).voiceDuration ?? (item as any).duration,
@@ -318,31 +338,53 @@ function mapNoFriendToMessageItem(item: NoFriendMessageItem): MessageItem {
   };
 }
 
-async function fetchNoFriendMessage() {
+// 未关注人消息未读数:使用 message/getStrangerMessageNum 接口,取 data.notReadCount
+async function fetchUnfollowedUnreadCount() {
   const receiverId = getReceiverId();
   if (!receiverId) {
-    messageListByCategory.value.unfollowed = [];
     const cat = messageCategories.value.find(c => c.key === "unfollowed");
     if (cat) cat.unread = 0;
     return;
   }
+  try {
+    const res: any = await getStrangerMessageNum({ receiverId: "store_" + receiverId });
+    if (res?.mas == "暂无承载数据" || res?.msg == "暂无承载数据") {
+      const cat = messageCategories.value.find(c => c.key === "unfollowed");
+      if (cat) cat.unread = 0;
+      return;
+    }
+    const data = res?.data ?? res;
+    const notReadCount = data?.notReadCount ?? 0;
+    const cat = messageCategories.value.find(c => c.key === "unfollowed");
+    if (cat) cat.unread = Number(notReadCount) || 0;
+  } catch (e) {
+    const cat = messageCategories.value.find(c => c.key === "unfollowed");
+    if (cat) cat.unread = 0;
+  }
+}
+
+// 陌生人/未关注人消息列表:使用 message/getMessageList,friendType=2(与商家端 message-notFriend 一致)
+async function fetchNoFriendMessage() {
+  const receiverId = getReceiverId();
+  if (!receiverId) {
+    messageListByCategory.value.unfollowed = [];
+    await fetchUnfollowedUnreadCount();
+    return;
+  }
   messageLoading.value = true;
   try {
-    const res: any = await getNoFriendMessage({ receiverId: "store_" + receiverId });
+    const res: any = await getMessageList({ receiverId: "store_" + receiverId, friendType: 2 });
     const data = res?.data ?? res;
-    if (data.msg == "暂无承载数据") {
+    if (data?.msg == "暂无承载数据") {
       messageListByCategory.value.unfollowed = [];
+      const cat = messageCategories.value.find(c => c.key === "unfollowed");
+      if (cat) cat.unread = 0;
       return;
     }
     const rawList = Array.isArray(data) ? data : data ? [data] : [];
     const list: MessageItem[] = rawList.map((item: NoFriendMessageItem) => mapNoFriendToMessageItem(item));
     messageListByCategory.value.unfollowed = list;
-    const unreadTotal = rawList.reduce(
-      (sum: number, item: NoFriendMessageItem) => sum + (item.notReadCount ?? (item.isRead === 0 ? 1 : 0)),
-      0
-    );
-    const cat = messageCategories.value.find(c => c.key === "unfollowed");
-    if (cat) cat.unread = unreadTotal;
+    await fetchUnfollowedUnreadCount();
   } catch (e) {
     messageListByCategory.value.unfollowed = [];
     const cat = messageCategories.value.find(c => c.key === "unfollowed");
@@ -352,7 +394,7 @@ async function fetchNoFriendMessage() {
   }
 }
 
-// 消息列表:/message/getMessageList,参数 friendType 默认 0、receiverId,返回结构与 getNoFriendMessage 一致
+// 消息列表:/message/getMessageList,friendType=1 仅返回已关注/好友消息(不包含未关注人,与商家端一致)
 async function fetchMessageList() {
   const receiverId = getReceiverId();
   if (!receiverId) {
@@ -363,7 +405,7 @@ async function fetchMessageList() {
   }
   messageLoading.value = true;
   try {
-    const res: any = await getMessageList({ receiverId: "store_" + receiverId, friendType: 0 });
+    const res: any = await getMessageList({ receiverId: "store_" + receiverId, friendType: 1 });
     const data = res?.data ?? res;
     const rawList = Array.isArray(data) ? data : data ? [data] : [];
     const list: MessageItem[] = rawList.map((item: NoFriendMessageItem) => mapNoFriendToMessageItem(item));
@@ -388,10 +430,16 @@ function getReceiverId(): string {
   return localGet("iphone") || localGet("geeker-user")?.userInfo?.phone || "";
 }
 
-// 点击消息列表项:关闭抽屉并跳转聊天页(与 storeDecoration 装修聊天一致)
+// 未读数量展示:超过 99 显示 99(与商家端 handleReadCount 一致)
+function handleReadCount(val: number): string {
+  if (Number(val) > 99) return "99";
+  return String(val);
+}
+
+// 点击消息列表项:关闭抽屉并跳转聊天页(与 storeDecoration 装修聊天一致,receiverId 用 phoneId)
 function handleMessageItemClick(item: MessageItem) {
-  console.log("item", item);
-  const receiverId = item.senderName;
+  const receiverId = item.phoneId ?? item.senderId;
+
   if (!receiverId) return;
   emit("close");
   const params = new URLSearchParams({
@@ -577,6 +625,20 @@ function handleDelete(item: NoticeItem, index: number) {
   const cat = noticeCategories.value.find(c => c.key === key);
   if (cat && item.unread) cat.unread = Math.max(0, cat.unread - 1);
 }
+/** 刷新全部(供 WebSocket 消息到达时实时更新,与商家端一致) */
+function refresh() {
+  if (activeTab.value === "notice") {
+    fetchNoticeUnreadCounts();
+    fetchNoticeList(activeCategory.value);
+  } else {
+    fetchUnfollowedUnreadCount();
+    fetchNoFriendMessage();
+    fetchMessageList();
+  }
+}
+
+defineExpose({ refresh });
+
 onMounted(() => {
   refreshAllNoticeCategories();
 });
@@ -740,14 +802,51 @@ watch(activeTab, val => {
     }
   }
   .message-date {
+    flex-shrink: 0;
     font-size: 12px;
     color: var(--el-text-color-secondary);
   }
-  .message-content {
+  .message-content-row {
+    display: flex;
+    gap: 10px;
+    align-items: center;
+    justify-content: space-between;
     margin-bottom: 8px;
+  }
+  .message-content {
+    flex: 1;
+    min-width: 0;
+    overflow: hidden;
     font-size: 14px;
     line-height: 1.5;
     color: var(--el-text-color-regular);
+    text-overflow: ellipsis;
+    white-space: nowrap;
+  }
+  .message-status {
+    position: relative;
+    display: flex;
+    flex-shrink: 0;
+    align-items: center;
+  }
+  .message-count {
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+    min-width: 20px;
+    height: 20px;
+    padding: 0 6px;
+    font-size: 12px;
+    font-weight: 500;
+    color: #ffffff;
+    background: #ff2442;
+    border-radius: 10px;
+  }
+  .message-dot {
+    width: 8px;
+    height: 8px;
+    background: var(--el-color-danger);
+    border-radius: 50%;
   }
   .message-actions {
     display: flex;

+ 1 - 1
src/views/dynamicManagement/userDynamic.vue

@@ -1354,7 +1354,7 @@ const handleGiftCouponSubmit = async () => {
       };
     } else if (giftCouponFormData.giftType === "voucher") {
       params = {
-        voucherIds: validRows.map(r => ({
+        couponIds: validRows.map(r => ({
           voucherId: r.couponId,
           singleQty: r.quantity
         })),

+ 20 - 6
src/views/storeDecoration/decorationChat.vue

@@ -159,7 +159,7 @@ const handleSend = async () => {
   const ok = await socketStore.sendMessage({
     category: "message",
     type: 1,
-    receiverId: "store_" + receiverId.value,
+    receiverId: receiverId.value,
     senderId: sendId.value,
     text
   });
@@ -172,7 +172,7 @@ const handleSend = async () => {
         type: 1,
         content: text,
         senderId: sendId.value,
-        receiverId: "store_" + receiverId.value,
+        receiverId: receiverId.value,
         createdTime: formatTime(new Date())
       }
     ];
@@ -220,7 +220,7 @@ const handleImageSelect = async (e: Event) => {
     const ok = await socketStore.sendMessage({
       category: "message",
       type: 2,
-      receiverId: "store_" + receiverId.value,
+      receiverId: receiverId.value,
       senderId: sendId.value,
       text: fileUrl
     });
@@ -270,7 +270,7 @@ const handleVideoSelect = async (e: Event) => {
     const ok = await socketStore.sendMessage({
       category: "message",
       type: 8,
-      receiverId: "store_" + receiverId.value,
+      receiverId: receiverId.value,
       senderId: sendId.value,
       text: fileUrl
     });
@@ -298,6 +298,19 @@ const handleVideoSelect = async (e: Event) => {
   }
 };
 
+// 消息已读(与商家端 chat.vue 一致:receiverId=当前登录人,senderId=当前聊天人)
+const readMessage = async () => {
+  if (!receiverId.value || !sendId.value) return;
+  try {
+    await messageRead({
+      receiverId: sendId.value,
+      senderId: receiverId.value
+    });
+  } catch (e) {
+    console.error("消息已读接口调用失败", e);
+  }
+};
+
 const loadChatRecord = async () => {
   if (!receiverId.value || !sendId.value) {
     loading.value = false;
@@ -305,10 +318,10 @@ const loadChatRecord = async () => {
   }
   try {
     loading.value = true;
-    await messageRead({ receiverId: sendId.value, senderId: receiverId.value });
+    await readMessage();
     const res: any = await getChatRecord({
       receiverId: sendId.value,
-      senderId: "store_" + receiverId.value
+      senderId: receiverId.value
     });
     const list = res?.data?.messageList || res?.messageList || [];
     list.forEach((item: any) => {
@@ -450,6 +463,7 @@ onMounted(async () => {
 });
 
 onBeforeUnmount(() => {
+  readMessage();
   if (cleanMessageFn) {
     cleanMessageFn();
     cleanMessageFn = null;