topfans/frontend/pages/support-activity/composables/useMessageRealtime.js

128 lines
5.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<string|number>} 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) {
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,
}
}