|
|
@@ -43,9 +43,8 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
- <!-- 未读红点 -->
|
|
|
- <div v-if="chat.unread_count > 0" class="unread-badge">
|
|
|
- {{ chat.unread_count > 99 ? '99+' : chat.unread_count }}
|
|
|
+ <div v-if="(messageStore.unreadMap[chat.user_id] || 0) > 0" class="unread-badge">
|
|
|
+ {{ messageStore.unreadMap[chat.user_id] > 99 ? '99+' : messageStore.unreadMap[chat.user_id] }}
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
@@ -231,14 +230,16 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
-import { ref, onMounted, computed, nextTick } from 'vue'
|
|
|
+import { ref, onMounted, onUnmounted, computed, nextTick } from 'vue'
|
|
|
import UserAvatar from '@/components/UserAvatar.vue'
|
|
|
import { Picture, Document, House, ArrowRight } from '@element-plus/icons-vue'
|
|
|
import api from '@/utils/request'
|
|
|
import { useAuthStore } from '@/store/auth'
|
|
|
+import { useMessageStore, onNewMessage } from '@/store/message'
|
|
|
import { ElMessage } from 'element-plus'
|
|
|
|
|
|
const authStore = useAuthStore()
|
|
|
+const messageStore = useMessageStore()
|
|
|
const currentUser = computed(() => authStore.user)
|
|
|
const currentUserId = computed(() => authStore.user?.id)
|
|
|
|
|
|
@@ -271,95 +272,57 @@ const filteredConversations = computed(() => {
|
|
|
)
|
|
|
})
|
|
|
|
|
|
+// WS message handler (called by store when a new message arrives)
|
|
|
+const handleWsMessage = (newMessage: any) => {
|
|
|
+ const isSystemNotification = newMessage.type === 'NOTIFICATION' || newMessage.sender_id === null
|
|
|
+ const systemConvId = isSystemNotification && newMessage.app_id ? -newMessage.app_id : 0
|
|
|
+
|
|
|
+ if (isSystemNotification && currentChatId.value === systemConvId) {
|
|
|
+ messages.value.push(newMessage)
|
|
|
+ scrollToBottom()
|
|
|
+ } else if (
|
|
|
+ currentChatId.value === newMessage.sender_id ||
|
|
|
+ (newMessage.sender_id === currentUserId.value && currentChatId.value === newMessage.receiver_id)
|
|
|
+ ) {
|
|
|
+ messages.value.push(newMessage)
|
|
|
+ scrollToBottom()
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isSystemNotification) {
|
|
|
+ updateConversationPreview(systemConvId, newMessage.content, newMessage.content_type, {
|
|
|
+ is_system: true,
|
|
|
+ app_id: newMessage.app_id,
|
|
|
+ app_name: newMessage.app_name
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ updateConversationPreview(
|
|
|
+ newMessage.sender_id === currentUserId.value ? newMessage.receiver_id : newMessage.sender_id,
|
|
|
+ newMessage.content,
|
|
|
+ newMessage.content_type
|
|
|
+ )
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
// Lifecycle
|
|
|
+let unsubscribeWs: (() => void) | null = null
|
|
|
+
|
|
|
onMounted(() => {
|
|
|
fetchConversations()
|
|
|
if (!currentUser.value) authStore.fetchUser()
|
|
|
+ unsubscribeWs = onNewMessage(handleWsMessage)
|
|
|
})
|
|
|
|
|
|
-// Methods
|
|
|
-const initWebSocket = () => {
|
|
|
- if (!currentUser.value) return
|
|
|
-
|
|
|
- const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
|
|
- // Use relative path or configured base URL
|
|
|
- const wsUrl = `${protocol}//${window.location.host}/api/v1/ws/messages?token=${localStorage.getItem('token')}`
|
|
|
-
|
|
|
- const ws = new WebSocket(wsUrl)
|
|
|
-
|
|
|
- ws.onmessage = (event) => {
|
|
|
- if (event.data === 'pong') return
|
|
|
- try {
|
|
|
- const msg = JSON.parse(event.data)
|
|
|
- if (msg.type === 'NEW_MESSAGE') {
|
|
|
- const newMessage = msg.data
|
|
|
- const isSystemNotification = newMessage.type === 'NOTIFICATION' || newMessage.sender_id === null
|
|
|
-
|
|
|
- // 系统会话 ID:与后端约定一致,按 app 拆分
|
|
|
- const systemConvId = isSystemNotification && newMessage.app_id ? -newMessage.app_id : 0
|
|
|
-
|
|
|
- // 如果当前会话打开,直接追加到消息流
|
|
|
- if (isSystemNotification && currentChatId.value === systemConvId) {
|
|
|
- messages.value.push(newMessage)
|
|
|
- scrollToBottom()
|
|
|
- } else if (
|
|
|
- currentChatId.value === newMessage.sender_id ||
|
|
|
- (newMessage.sender_id === currentUserId.value && currentChatId.value === newMessage.receiver_id)
|
|
|
- ) {
|
|
|
- // 私信:当前窗口就是发送者或接收者
|
|
|
- messages.value.push(newMessage)
|
|
|
- scrollToBottom()
|
|
|
- } else if (newMessage.sender_id === currentUserId.value && currentChatId.value === newMessage.receiver_id) {
|
|
|
- // 我在其他标签发送的消息
|
|
|
- messages.value.push(newMessage)
|
|
|
- scrollToBottom()
|
|
|
- }
|
|
|
-
|
|
|
- // 更新左侧会话预览
|
|
|
- if (isSystemNotification) {
|
|
|
- updateConversationPreview(systemConvId, newMessage.content, newMessage.content_type, {
|
|
|
- is_system: true,
|
|
|
- app_id: newMessage.app_id,
|
|
|
- app_name: newMessage.app_name
|
|
|
- })
|
|
|
- } else {
|
|
|
- updateConversationPreview(
|
|
|
- newMessage.sender_id === currentUserId.value ? newMessage.receiver_id : newMessage.sender_id,
|
|
|
- newMessage.content,
|
|
|
- newMessage.content_type
|
|
|
- )
|
|
|
- }
|
|
|
-
|
|
|
- // 未读计数(当前未打开该会话时)
|
|
|
- if (isSystemNotification) {
|
|
|
- if (currentChatId.value !== systemConvId) {
|
|
|
- const conv = conversations.value.find(c => c.user_id === systemConvId)
|
|
|
- if (conv) conv.unread_count = (conv.unread_count || 0) + 1
|
|
|
- }
|
|
|
- } else if (newMessage.sender_id !== currentUserId.value && currentChatId.value !== newMessage.sender_id) {
|
|
|
- const conv = conversations.value.find(c => c.user_id === newMessage.sender_id)
|
|
|
- if (conv) conv.unread_count = (conv.unread_count || 0) + 1
|
|
|
- }
|
|
|
- }
|
|
|
- } catch (e) {
|
|
|
- console.error('WS parse error', e)
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- ws.onopen = () => {
|
|
|
- // Start heartbeat
|
|
|
- setInterval(() => {
|
|
|
- if (ws.readyState === WebSocket.OPEN) ws.send('ping')
|
|
|
- }, 30000)
|
|
|
- }
|
|
|
-}
|
|
|
+onUnmounted(() => {
|
|
|
+ messageStore.setCurrentChat(null)
|
|
|
+ if (unsubscribeWs) unsubscribeWs()
|
|
|
+})
|
|
|
|
|
|
+// Methods
|
|
|
const fetchConversations = async () => {
|
|
|
loadingConversations.value = true
|
|
|
try {
|
|
|
const res = await api.get('/messages/conversations')
|
|
|
conversations.value = res.data
|
|
|
- initWebSocket() // Start WS after data loaded
|
|
|
} catch (e) {
|
|
|
console.error(e)
|
|
|
} finally {
|
|
|
@@ -370,37 +333,15 @@ const fetchConversations = async () => {
|
|
|
const selectChat = async (chat: any) => {
|
|
|
currentChatId.value = chat.user_id
|
|
|
currentChatUser.value = chat
|
|
|
-
|
|
|
- // Mark as read locally (API call typically happens here or on scroll)
|
|
|
- // For simplicity, we just load history
|
|
|
+ messageStore.setCurrentChat(chat.user_id)
|
|
|
await loadHistory(chat.user_id)
|
|
|
-
|
|
|
- // Clear unread count locally
|
|
|
- const conv = conversations.value.find(c => c.user_id === chat.user_id)
|
|
|
- if (conv) conv.unread_count = 0
|
|
|
}
|
|
|
|
|
|
const loadHistory = async (userId: number) => {
|
|
|
try {
|
|
|
const res = await api.get(`/messages/history/${userId}`)
|
|
|
- // API returns newest first, reverse for display
|
|
|
messages.value = res.data.reverse()
|
|
|
scrollToBottom()
|
|
|
-
|
|
|
- // Mark messages as read
|
|
|
- // Iterate and find unread... or just call a "read all from this user" endpoint?
|
|
|
- const unreadIds = messages.value.filter(m => !m.is_read && m.receiver_id === currentUserId.value).map(m => m.id)
|
|
|
- if (unreadIds.length > 0) {
|
|
|
- // Implement batch read
|
|
|
- // For efficiency, we mark one by one for now, or ideally backend supports batch
|
|
|
- // Using Promise.all to parallelize
|
|
|
- await Promise.all(unreadIds.map(id => api.put(`/messages/${id}/read`)))
|
|
|
-
|
|
|
- // Update local state for unread count
|
|
|
- const conv = conversations.value.find(c => c.user_id === userId)
|
|
|
- if (conv) conv.unread_count = 0
|
|
|
- }
|
|
|
-
|
|
|
} catch (e) {
|
|
|
console.error(e)
|
|
|
}
|