import { ref, computed, onMounted, onUnmounted } from 'vue' import { useStore } from 'vuex' import { listActivityMessagesApi, createActivityMessageApi } from '@/utils/api.js' import { getActivitySocket } from '@/utils/socket/ActivitySocket.js' import { formatRelativeTime } from '@/utils/format.js' const MAX_MESSAGES = 50 /** * 活动留言实时推送 composable * @param {import('vue').Ref} activityId */ export function useMessageRealtime(activityId) { const store = useStore() const messages = ref([]) const loading = ref(false) const error = ref(null) const currentUserId = computed(() => store.state.user?.userInfo?.uid) const socket = getActivitySocket() // 字段映射:后端 → MessageBoard props function toComponentShape(m) { return { id: m.id, user: m.nickname || '', avatar: m.avatar_url || '', content: m.content, time: formatRelativeTime(m.created_at), isSelf: m.user_id === currentUserId.value, } } async function loadHistory() { if (!activityId.value) return loading.value = true error.value = null try { const res = await listActivityMessagesApi(activityId.value, 1, 20) if (res.code === 0) { messages.value = (res.data.messages || []).map(toComponentShape) } else { error.value = res.message || '加载留言失败' } } catch (e) { error.value = e.message || '网络错误' console.error('[useMessageRealtime] loadHistory error:', e) } finally { loading.value = false } } function onWsMessage(payload) { if (!payload || Number(payload.activity_id) !== Number(activityId.value)) return if (!payload.message) return messages.value.push(toComponentShape(payload.message)) if (messages.value.length > MAX_MESSAGES) { messages.value.splice(0, messages.value.length - MAX_MESSAGES) } } async function sendMessage(content) { if (!content || !content.trim()) return const trimmed = content.trim() try { const res = await createActivityMessageApi(activityId.value, trimmed) if (res.code === 0) { // WS 推回时会再次触发 onMessage → 列表会出现该留言 // 断线时本地 fallback 插入 if (!socket.isConnected && res.data?.message) { messages.value.push(toComponentShape(res.data.message)) if (messages.value.length > MAX_MESSAGES) { messages.value.splice(0, messages.value.length - MAX_MESSAGES) } } // 无论 WS 还是 fallback,都弹一个轻量 toast 提示 // uni.showToast({ title: '留言成功', icon: 'success', duration: 1200 }) } else { uni.showToast({ title: res.message || '留言失败', icon: 'none' }) } } catch (e) { console.error('[useMessageRealtime] sendMessage error:', e) uni.showToast({ title: '网络错误', icon: 'none' }) } } onMounted(() => { loadHistory() // 总是调用 connect():SocketManager.connect() 内部会判断 token 是否变化。 // - 同 token + 已连接 → _doConnect 空跑,无副作用 // - 异 token(用户切换登录) → _hardReset 关闭旧连接、强制重连 // 这样即使旧用户 WS 还活着但 token 已变,也能拿到新用户自己的连接, // 解决"另一个账户没有接收到实时推送"的问题。 const token = uni.getStorageSync('access_token') if (token) { // 连接统一由页面 onShow 管理 // socket.connect(token).catch(err => console.warn('[useMessageRealtime] connect error:', err)) } // 同步分支:如果 WS 已连接(单例复用,另一个 composable 可能先连上了), // subscribe 会直接发送;否则缓存到 _topics,等待 connect 事件触发 resubscribeAll socket.subscribe(activityId.value, ['messages']) socket.onMessagesResponse(onWsMessage) // 如果后端 pub/sub 被禁用,WS 订阅无效,需要手动刷新才能看到新消息 if (!socket.isPubSubEnabled()) { console.warn('[useMessageRealtime] Pub/Sub disabled, real-time messages unavailable. Pull-to-refresh to see new messages.') } }) // 后端 pub/sub 状态变化:禁用时提醒用户 function onPubsubDisabled() { console.warn('[useMessageRealtime] Pub/Sub became disabled, new messages will not arrive in real-time') } socket.on('pubsub_disabled', onPubsubDisabled) onUnmounted(() => { socket.unsubscribe(activityId.value, ['messages']) socket.offMessagesResponse(onWsMessage) socket.off('pubsub_disabled', onPubsubDisabled) }) return { messages, loading, error, sendMessage, refresh: loadHistory, } }