114 lines
4.2 KiB
JavaScript
114 lines
4.2 KiB
JavaScript
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))
|
||
}
|
||
socket.subscribe(activityId.value, ['messages'])
|
||
socket.onMessagesResponse(onWsMessage)
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
socket.unsubscribe(activityId.value, ['messages'])
|
||
socket.offMessagesResponse(onWsMessage)
|
||
})
|
||
|
||
return {
|
||
messages,
|
||
loading,
|
||
error,
|
||
sendMessage,
|
||
refresh: loadHistory,
|
||
}
|
||
}
|