/** * 会话消息:拉取历史、加载更多、发文字、发文件 */ 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 } }