朋友圈
This commit is contained in:
@@ -21,6 +21,9 @@ class WebSocketManager {
|
||||
private reconnectAttempts = 0
|
||||
private maxReconnectAttempts = 5
|
||||
private reconnectDelay = 3000
|
||||
// 用于通知去重的缓存(存储最近处理过的通知ID)
|
||||
private recentMomentNotifs: Set<string> = new Set()
|
||||
private notifCacheTimeout = 5000 // 5秒内相同通知视为重复
|
||||
|
||||
/**
|
||||
* 连接WebSocket
|
||||
@@ -119,7 +122,10 @@ class WebSocketManager {
|
||||
* 添加普通消息处理器
|
||||
*/
|
||||
onMessage(handler: MessageHandler) {
|
||||
this.messageHandlers.push(handler)
|
||||
// 防止重复注册
|
||||
if (!this.messageHandlers.includes(handler)) {
|
||||
this.messageHandlers.push(handler)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -136,7 +142,10 @@ class WebSocketManager {
|
||||
* 添加信令处理器 (WebRTC用)
|
||||
*/
|
||||
onSignal(handler: SignalHandler) {
|
||||
this.signalHandlers.push(handler)
|
||||
// 防止重复注册
|
||||
if (!this.signalHandlers.includes(handler)) {
|
||||
this.signalHandlers.push(handler)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -153,7 +162,10 @@ class WebSocketManager {
|
||||
* 添加朋友圈通知处理器
|
||||
*/
|
||||
onMomentNotification(handler: MomentNotifHandler) {
|
||||
this.momentNotifHandlers.push(handler)
|
||||
// 防止重复注册
|
||||
if (!this.momentNotifHandlers.includes(handler)) {
|
||||
this.momentNotifHandlers.push(handler)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -167,9 +179,27 @@ class WebSocketManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* 内部处理朋友圈通知
|
||||
* 内部处理朋友圈通知(带去重)
|
||||
*/
|
||||
private handleMomentNotification(payload: MomentNotifPayload) {
|
||||
// 生成通知唯一标识(基于 moment_id + type + from_user_id)
|
||||
const notifKey = `${payload.moment_id}_${payload.type}_${payload.from_user?.id || ''}_${payload.comment_id || ''}`
|
||||
|
||||
// 检查是否为重复通知
|
||||
if (this.recentMomentNotifs.has(notifKey)) {
|
||||
console.log('🔄 跳过重复朋友圈通知:', notifKey)
|
||||
return
|
||||
}
|
||||
|
||||
// 添加到缓存
|
||||
this.recentMomentNotifs.add(notifKey)
|
||||
|
||||
// 设置定时器清除缓存
|
||||
setTimeout(() => {
|
||||
this.recentMomentNotifs.delete(notifKey)
|
||||
}, this.notifCacheTimeout)
|
||||
|
||||
// 分发给所有处理器
|
||||
this.momentNotifHandlers.forEach(handler => handler(payload))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div>
|
||||
<img
|
||||
<img
|
||||
:src="fullImageUrl"
|
||||
class="max-w-full cursor-zoom-in hover:brightness-90 transition duration-300 block max-h-[300px] object-cover rounded-lg"
|
||||
loading="lazy"
|
||||
loading="lazy"
|
||||
@click="showPreview = true"
|
||||
/>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
:show="showPreview"
|
||||
:images="[fullImageUrl]"
|
||||
@close="showPreview = false"
|
||||
/>
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -64,7 +64,8 @@
|
||||
:name="moment.user?.name || ''"
|
||||
:avatar="moment.user?.avatar"
|
||||
size="xl"
|
||||
class="shrink-0"
|
||||
class="shrink-0 cursor-pointer hover:opacity-80 transition"
|
||||
@click="showUserInfo(moment.user)"
|
||||
/>
|
||||
<div class="flex-1 min-w-0">
|
||||
<!-- 名字和时间 -->
|
||||
@@ -142,8 +143,9 @@
|
||||
:name="like.user?.name || ''"
|
||||
:avatar="like.user?.avatar"
|
||||
size="sm"
|
||||
class="shrink-0"
|
||||
class="shrink-0 cursor-pointer hover:opacity-80 transition"
|
||||
:title="like.user?.name"
|
||||
@click="showUserInfo(like.user)"
|
||||
/>
|
||||
</template>
|
||||
<span v-if="moment.likes.length > 8" class="text-gray-500 text-xs ml-1">
|
||||
@@ -158,21 +160,35 @@
|
||||
<div
|
||||
v-for="comment in moment.comments.slice(0, expandedMoments.has(moment.id) ? undefined : 3)"
|
||||
:key="comment.id"
|
||||
class="flex items-start gap-2 p-2 bg-white/5 rounded-lg text-sm"
|
||||
class="flex items-start gap-2 p-2 bg-white/5 rounded-lg text-sm group/comment"
|
||||
>
|
||||
<Avatar
|
||||
:name="comment.user?.name || ''"
|
||||
:avatar="comment.user?.avatar"
|
||||
size="sm"
|
||||
class="shrink-0"
|
||||
class="shrink-0 cursor-pointer hover:opacity-80 transition"
|
||||
@click="showUserInfo(comment.user)"
|
||||
/>
|
||||
<div class="flex-1 min-w-0">
|
||||
<span class="font-medium text-gray-300">{{ comment.user?.name }}</span>
|
||||
<span
|
||||
class="font-medium text-gray-300 cursor-pointer hover:text-primary transition"
|
||||
@click="showUserInfo(comment.user)"
|
||||
>{{ comment.user?.name }}</span>
|
||||
<span v-if="comment.reply_to_user" class="text-gray-500">
|
||||
回复 <span class="text-gray-400">{{ comment.reply_to_user.name }}</span>
|
||||
回复 <span
|
||||
class="text-gray-400 cursor-pointer hover:text-primary transition"
|
||||
@click="showUserInfo(comment.reply_to_user)"
|
||||
>{{ comment.reply_to_user.name }}</span>
|
||||
</span>
|
||||
<span class="text-gray-500">: </span>
|
||||
<span class="text-gray-400 break-words">{{ comment.content }}</span>
|
||||
<!-- 回复按钮 -->
|
||||
<button
|
||||
@click.stop="openCommentInput(moment, comment)"
|
||||
class="ml-2 text-xs text-gray-500 hover:text-primary transition opacity-0 group-hover/comment:opacity-100"
|
||||
>
|
||||
回复
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 展开更多评论 -->
|
||||
@@ -254,29 +270,48 @@
|
||||
:initial-index="previewIndex"
|
||||
@close="showImagePreview = false"
|
||||
/>
|
||||
|
||||
<!-- 用户信息卡片 -->
|
||||
<UserInfoCard
|
||||
:show="showUserCard"
|
||||
:user="selectedUser"
|
||||
@close="showUserCard = false"
|
||||
@send-message="handleSendMessage"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, nextTick } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useMomentStore } from '@/stores/moment'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useChatStore } from '@/stores/chat'
|
||||
import Avatar from '@/components/common/Avatar.vue'
|
||||
import ImagePreview from '@/components/common/ImagePreview.vue'
|
||||
import UserInfoCard from '@/components/common/UserInfoCard.vue'
|
||||
import MomentPublisherDark from './MomentPublisherDark.vue'
|
||||
import MomentNotificationPanel from './MomentNotificationPanel.vue'
|
||||
import { parseMediaUrls } from '@/types/moment'
|
||||
import { formatRelativeTime } from '@/utils/format'
|
||||
import { storage } from '@/utils/storage'
|
||||
import type { Moment, MomentComment } from '@/types/moment'
|
||||
import type { User } from '@/types/api'
|
||||
|
||||
const router = useRouter()
|
||||
const momentStore = useMomentStore()
|
||||
const authStore = useAuthStore()
|
||||
const chatStore = useChatStore()
|
||||
|
||||
// 状态
|
||||
const showPublisher = ref(false)
|
||||
const showNotifications = ref(false)
|
||||
const expandedMoments = reactive(new Set<number>())
|
||||
|
||||
// 用户信息卡片相关
|
||||
const showUserCard = ref(false)
|
||||
const selectedUser = ref<User | null>(null)
|
||||
|
||||
// 评论相关
|
||||
const commentingMoment = ref<Moment | null>(null)
|
||||
const commentText = ref('')
|
||||
@@ -374,6 +409,34 @@ async function handleComment() {
|
||||
function onPublished() {
|
||||
showPublisher.value = false
|
||||
}
|
||||
|
||||
// 显示用户信息卡片
|
||||
function showUserInfo(user?: User | null) {
|
||||
if (!user) return
|
||||
selectedUser.value = user
|
||||
showUserCard.value = true
|
||||
}
|
||||
|
||||
// 从用户信息卡片发起聊天
|
||||
async function handleSendMessage() {
|
||||
if (!selectedUser.value) return
|
||||
|
||||
// 关闭用户信息卡片
|
||||
showUserCard.value = false
|
||||
|
||||
// 切换到聊天 Tab
|
||||
storage.setCurrentTab('chat')
|
||||
|
||||
// 查找或创建与该用户的会话
|
||||
const contact = chatStore.contacts.find(c => c.user_id === selectedUser.value?.id || c.id === selectedUser.value?.id)
|
||||
if (contact) {
|
||||
chatStore.setCurrentTarget(contact)
|
||||
storage.setSelectedConversation(contact.id)
|
||||
if (contact.room_id) {
|
||||
storage.setSelectedRoomId(contact.room_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -129,3 +129,4 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user