| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331 |
- /**
- * 会话消息:拉取历史、加载更多、发文字、发文件
- */
- import {
- getMessages,
- sendMessage as apiSendMessage,
- uploadFile,
- getToken,
- getContentType,
- getUserIdFromToken,
- normalizeMessageContentType
- } from '../utils/api'
- import { chatStore } from '../store/chat'
- /**
- * 归一化单条消息。若后端未返回 is_me,则用 currentUserId 与 sender_id 计算
- * @param {object} m - 原始消息
- * @param {string} [currentUserId] - 当前登录用户 ID,用于推算 isMe
- */
- function normalizeMessage(m, currentUserId) {
- if (!m) return null
- const type = m.type ?? 'MESSAGE'
- let isMe = m.is_me ?? m.isMe
- if (isMe === undefined && currentUserId != null && currentUserId !== '') {
- const senderId = m.sender_id ?? m.senderId
- isMe = String(senderId) === String(currentUserId)
- }
- const rawText = m.content ?? m.text ?? ''
- const urlField = m.url ?? m.file_url ?? m.content_url
- const content =
- urlField && /^https?:\/\//i.test(String(urlField)) ? String(urlField) : String(rawText)
- return {
- id: String(m.id),
- type,
- senderId: m.sender_id ?? m.senderId,
- receiverId: m.receiver_id ?? m.receiverId,
- content,
- contentType: normalizeMessageContentType(m.content_type ?? m.contentType ?? 'TEXT'),
- title: m.title,
- createdAt: m.created_at ?? m.createdAt,
- isMe: !!isMe,
- actionUrl: m.action_url ?? m.actionUrl,
- actionText: m.action_text ?? m.actionText
- }
- }
- function normalizeMessageList(list, currentUserId) {
- return (list || []).map((m) => normalizeMessage(m, currentUserId)).filter(Boolean)
- }
- /**
- * 拉取某会话最新一页历史(与聊天页 fetchMessages 同源逻辑)。
- * 供 WebSocket 重连后补拉断线期间消息,避免仅依赖推送延迟。
- */
- export async function fetchMessagesForContact(contactId) {
- const token = getToken()
- if (!token) return
- const currentUserId = getUserIdFromToken(token)
- try {
- const data = await getMessages(token, contactId, { skip: 0, limit: 50 })
- const list = Array.isArray(data) ? data : (data.messages || data.list || data.items || [])
- chatStore.setMessagesForContact(contactId, normalizeMessageList(list, currentUserId))
- chatStore.markContactLoaded(contactId)
- } catch (e) {
- chatStore.setMessagesForContact(contactId, [])
- }
- }
- export function useMessages() {
- async function fetchMessages(contactId) {
- return fetchMessagesForContact(contactId)
- }
- async function fetchMoreMessages(contactId) {
- const list = chatStore.messages[String(contactId)] || []
- const skip = list.length
- const token = getToken()
- if (!token) return
- chatStore.loadingMore[String(contactId)] = true
- try {
- const currentUserId = getUserIdFromToken(token)
- const data = await getMessages(token, contactId, { skip, limit: 50 })
- const more = Array.isArray(data) ? data : (data.messages || data.list || data.items || [])
- if (more.length) chatStore.prependMessages(contactId, normalizeMessageList(more, currentUserId))
- } catch (e) {
- // 静默失败
- } finally {
- chatStore.loadingMore[String(contactId)] = false
- }
- }
- async function sendMessage(contactId, content) {
- const text = (content || '').trim()
- if (!text) return
- const token = getToken()
- if (!token) {
- uni.showToast({ title: '请先登录', icon: 'none' })
- return
- }
- const cid = String(contactId)
- const tempId = 'temp-' + Date.now()
- const tempMsg = {
- id: tempId,
- tempId,
- senderId: null,
- content: text,
- contentType: 'TEXT',
- createdAt: new Date().toISOString(),
- isMe: true,
- status: 'sending'
- }
- chatStore.appendMessage(cid, tempMsg)
- try {
- const res = await apiSendMessage(token, cid, text, 'TEXT')
- const raw = (res && (res.data ?? res.message ?? res)) || res
- const currentUserId = getUserIdFromToken(token)
- const serverMsg = normalizeMessage(typeof raw === 'object' && raw !== null ? raw : null, currentUserId)
- const hasValidId = serverMsg && serverMsg.id && String(serverMsg.id) !== 'undefined'
- if (hasValidId) {
- const finalMsg = serverMsg.content != null && serverMsg.content !== ''
- ? serverMsg
- : { ...serverMsg, content: text, contentType: 'TEXT' }
- chatStore.replaceTempMessage(cid, tempId, finalMsg)
- } else {
- chatStore.updateMessage(cid, tempId, { status: 'sent' })
- }
- } catch (e) {
- chatStore.updateMessage(cid, tempId, { status: 'failed' })
- const errMsg = (e && (e.message || e.errMsg)) || '发送失败'
- uni.showToast({ title: errMsg, icon: 'none', duration: 3000 })
- }
- }
- async function retrySendMessage(contactId, msg) {
- if (!msg || !msg.tempId || msg.status !== 'failed') return
- const text = (msg.content || '').trim()
- if (!text) return
- const token = getToken()
- if (!token) {
- uni.showToast({ title: '请先登录', icon: 'none' })
- return
- }
- const cid = String(contactId)
- chatStore.updateMessage(cid, msg.tempId, { status: 'sending' })
- try {
- const res = await apiSendMessage(token, cid, text, 'TEXT')
- const raw = (res && (res.data ?? res.message ?? res)) || res
- const currentUserId = getUserIdFromToken(token)
- const serverMsg = normalizeMessage(typeof raw === 'object' && raw !== null ? raw : null, currentUserId)
- const hasValidId = serverMsg && serverMsg.id && String(serverMsg.id) !== 'undefined'
- if (hasValidId) {
- const finalMsg = serverMsg.content != null && serverMsg.content !== ''
- ? serverMsg
- : { ...serverMsg, content: text, contentType: 'TEXT' }
- chatStore.replaceTempMessage(cid, msg.tempId, finalMsg)
- } else {
- chatStore.updateMessage(cid, msg.tempId, { status: 'sent' })
- }
- } catch (e) {
- chatStore.updateMessage(cid, msg.tempId, { status: 'failed' })
- const errMsg = (e && (e.message || e.errMsg)) || '发送失败'
- uni.showToast({ title: errMsg, icon: 'none', duration: 3000 })
- }
- }
- async function sendFileMessage(contactId, filePath, contentType, fileName, onProgress) {
- const token = getToken()
- if (!token) {
- uni.showToast({ title: '请先登录', icon: 'none' })
- return
- }
- const cid = String(contactId)
- const apiCt = normalizeMessageContentType(contentType)
- const displayTitle =
- (fileName && String(fileName).trim()) ||
- (apiCt === 'IMAGE' ? '图片' : apiCt === 'VIDEO' ? '视频' : '文件')
- const tempId = 'temp-' + Date.now()
- const tempMsg = {
- id: tempId,
- tempId,
- senderId: null,
- content: filePath,
- contentType: apiCt,
- title: displayTitle,
- createdAt: new Date().toISOString(),
- isMe: true,
- status: 'sending',
- localFilePath: filePath,
- uploadProgress: 0
- }
- chatStore.appendMessage(cid, tempMsg)
- try {
- const uploadResult = await uploadFile(token, filePath, fileName, (p) => {
- if (typeof p === 'number' && p >= 0 && p <= 1) {
- chatStore.updateMessage(cid, tempId, { uploadProgress: p })
- }
- if (typeof onProgress === 'function') onProgress(p)
- })
- const key = uploadResult.key || uploadResult.file_key
- const name = (fileName && String(fileName).trim()) || uploadResult.filename || displayTitle
- if (!key) throw new Error('上传未返回 key')
- const res = await apiSendMessage(token, cid, key, apiCt, name)
- const raw = (res && (res.data ?? res.message ?? res)) || res
- const currentUserId = getUserIdFromToken(token)
- const serverMsg = normalizeMessage(typeof raw === 'object' && raw !== null ? raw : null, currentUserId)
- const hasValidId = serverMsg && serverMsg.id && String(serverMsg.id) !== 'undefined'
- if (hasValidId) {
- const finalMsg =
- serverMsg.content != null && String(serverMsg.content) !== ''
- ? serverMsg
- : { ...serverMsg, content: key, contentType: apiCt, title: name }
- chatStore.replaceTempMessage(cid, tempId, finalMsg)
- } else {
- chatStore.updateMessage(cid, tempId, { status: 'sent', uploadProgress: undefined })
- }
- } catch (e) {
- chatStore.updateMessage(cid, tempId, { status: 'failed', uploadProgress: undefined })
- const errMsg = (e && (e.message || e.errMsg)) || '发送失败'
- uni.showToast({ title: errMsg, icon: 'none', duration: 3000 })
- }
- }
- async function retrySendFileMessage(contactId, msg) {
- if (!msg || !msg.tempId || msg.status !== 'failed' || !msg.localFilePath) return
- const token = getToken()
- if (!token) {
- uni.showToast({ title: '请先登录', icon: 'none' })
- return
- }
- const cid = String(contactId)
- const tempId = msg.tempId
- const apiCt = normalizeMessageContentType(msg.contentType)
- const displayTitle = (msg.title && String(msg.title).trim()) || (apiCt === 'IMAGE' ? '图片' : apiCt === 'VIDEO' ? '视频' : '文件')
- chatStore.updateMessage(cid, tempId, { status: 'sending', uploadProgress: 0 })
- try {
- const uploadResult = await uploadFile(token, msg.localFilePath, undefined, (p) => {
- if (typeof p === 'number' && p >= 0 && p <= 1) {
- chatStore.updateMessage(cid, tempId, { uploadProgress: p })
- }
- })
- const key = uploadResult.key || uploadResult.file_key
- const name = uploadResult.filename || displayTitle
- if (!key) throw new Error('上传未返回 key')
- const res = await apiSendMessage(token, cid, key, apiCt, name)
- const raw = (res && (res.data ?? res.message ?? res)) || res
- const currentUserId = getUserIdFromToken(token)
- const serverMsg = normalizeMessage(typeof raw === 'object' && raw !== null ? raw : null, currentUserId)
- const hasValidId = serverMsg && serverMsg.id && String(serverMsg.id) !== 'undefined'
- if (hasValidId) {
- const finalMsg =
- serverMsg.content != null && String(serverMsg.content) !== ''
- ? serverMsg
- : { ...serverMsg, content: key, contentType: apiCt, title: name }
- chatStore.replaceTempMessage(cid, tempId, finalMsg)
- } else {
- chatStore.updateMessage(cid, tempId, { status: 'sent', uploadProgress: undefined })
- }
- } catch (e) {
- chatStore.updateMessage(cid, tempId, { status: 'failed', uploadProgress: undefined })
- const errMsg = (e && (e.message || e.errMsg)) || '发送失败'
- uni.showToast({ title: errMsg, icon: 'none', duration: 3000 })
- }
- }
- /** 再次提醒:向对方重发一条相同结构的 USER_NOTIFICATION */
- async function remindUserNotification(contactId, sourceMsg) {
- if (!sourceMsg || sourceMsg.tempId) return
- const token = getToken()
- if (!token) {
- uni.showToast({ title: '请先登录', icon: 'none' })
- return
- }
- const cid = String(contactId)
- const tempId = 'temp-' + Date.now()
- const content = sourceMsg.content != null ? String(sourceMsg.content) : ''
- const title =
- sourceMsg.title != null && sourceMsg.title !== '' ? String(sourceMsg.title) : undefined
- const tempMsg = {
- id: tempId,
- tempId,
- senderId: null,
- type: 'MESSAGE',
- content,
- contentType: 'USER_NOTIFICATION',
- title: sourceMsg.title,
- actionUrl: sourceMsg.actionUrl,
- actionText: sourceMsg.actionText,
- createdAt: new Date().toISOString(),
- isMe: true,
- status: 'sending'
- }
- chatStore.appendMessage(cid, tempMsg)
- try {
- const res = await apiSendMessage(token, cid, content, 'USER_NOTIFICATION', title, {
- action_url: sourceMsg.actionUrl,
- action_text: sourceMsg.actionText
- })
- const raw = (res && (res.data ?? res.message ?? res)) || res
- const currentUserId = getUserIdFromToken(token)
- const serverMsg = normalizeMessage(typeof raw === 'object' && raw !== null ? raw : null, currentUserId)
- const hasValidId = serverMsg && serverMsg.id && String(serverMsg.id) !== 'undefined'
- if (hasValidId) {
- const finalMsg =
- serverMsg.contentType === 'USER_NOTIFICATION'
- ? serverMsg
- : { ...serverMsg, contentType: 'USER_NOTIFICATION', content, title: sourceMsg.title }
- chatStore.replaceTempMessage(cid, tempId, finalMsg)
- } else {
- chatStore.updateMessage(cid, tempId, { status: 'sent' })
- }
- } catch (e) {
- chatStore.removeMessage(cid, tempId)
- const errMsg = (e && (e.message || e.errMsg)) || '发送失败'
- uni.showToast({ title: errMsg, icon: 'none', duration: 3000 })
- }
- }
- return {
- messages: chatStore.messages,
- loadingMore: chatStore.loadingMore,
- fetchMessages,
- fetchMoreMessages,
- sendMessage,
- retrySendMessage,
- sendFileMessage,
- retrySendFileMessage,
- remindUserNotification,
- hasContactLoaded: (id) => chatStore.hasContactLoaded(id),
- getContentType
- }
- }
|