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) { socket.connect(token).catch(err => console.warn('[useMessageRealtime] connect error:', err)) } socket.subscribe(activityId.value, ['messages']) socket.onMessagesResponse(onWsMessage) }) onUnmounted(() => { socket.unsubscribe(activityId.value, ['messages']) socket.offMessagesResponse(onWsMessage) }) return { messages, loading, error, sendMessage, refresh: loadHistory, } }