/** * 聊天详情页 U24(TypeScript) * * 不变量(AGENTS.md): * - 第 3 条:Mutual Match 后才能聊天 * - 第 4 条:Block/Unmatch 后会话 canSendMessage=false * - 第 6 条:单向活动意向不泄露 */ // eslint-disable-next-line @typescript-eslint/no-unused-vars import { messageStore, appStore, blockUser } from '../../stores'; // eslint-disable-next-line @typescript-eslint/no-unused-vars import { messageService } from '../../services'; // eslint-disable-next-line @typescript-eslint/no-unused-vars import { to, ROUTE, DANGER_ACTION, BUSINESS_ERROR_CODE } from '../../constants'; // eslint-disable-next-line @typescript-eslint/no-unused-vars import { formatMessageTime } from '../../utils/datetime'; import type { Message } from '../../types'; /** 超过该间隔则显示时间分隔(5 分钟) */ const TIME_GAP_MS = 5 * 60 * 1000; interface ChatData { conversationId: string; otherDisplayName: string; otherUserId: string; otherAvatarUrl: string; canSend: boolean; messages: Array; hasMore: boolean; loading: boolean; loadingMore: boolean; sending: boolean; inputText: string; scrollIntoView: string; statusBarHeight: number; navBarHeight: number; showDangerDialog: boolean; dangerAction: string; connectionState: string; } Page({ data: { conversationId: '', otherDisplayName: '', otherUserId: '', otherAvatarUrl: '', canSend: true, messages: [], hasMore: true, loading: false, loadingMore: false, sending: false, inputText: '', scrollIntoView: '', statusBarHeight: 20, navBarHeight: 44, showDangerDialog: false, dangerAction: DANGER_ACTION.BLOCK, connectionState: 'disconnected', }, onLoad(options: Record) { const conversationId = options.id || ''; if (!conversationId) { appStore.toast('会话不存在'); wx.navigateBack(); return; } try { const info = (wx.getWindowInfo && wx.getWindowInfo()) || wx.getSystemInfoSync(); this.setData({ statusBarHeight: (info as any).statusBarHeight || 20, navBarHeight: (info as any).platform === 'android' ? 48 : 44, }); } catch { /* 默认值 */ } this.setData({ conversationId }); this._unsubConn = messageStore.bindToPage(this, { connectionState: 'connectionState' }); this._unsubList = messageStore.subscribe((state: any) => this._syncFromStore(state)); this._unsubRealtime = messageStore.onMessage(conversationId, (msg: Message) => { if (msg.bubbleType === 'other') { messageStore.markConversationRead(conversationId).catch(() => {}); } }); messageStore.openConversation(conversationId).catch((err: unknown) => { appStore.toastError(err instanceof Error ? err.message : '加载失败'); }); this.loadConversationMeta(); }, onUnload() { this._unsubConn && this._unsubConn(); this._unsubList && this._unsubList(); this._unsubRealtime && this._unsubRealtime(); if (this.data.conversationId) { messageStore.closeConversation(); } }, async loadConversationMeta() { try { // @ts-ignore const conv = await messageService.getConversation(this.data.conversationId); if (conv) { this.setData({ otherDisplayName: (conv as any).targetDisplayName || '用户', otherUserId: (conv as any).targetUserId || '', otherAvatarUrl: (conv as any).targetAvatarUrl || '', canSend: (conv as any).canSendMessage !== false, }); } } catch (err: unknown) { const local = (messageStore.state as any).conversations?.find( (c: any) => c.conversationId === this.data.conversationId, ); if (local) { this.setData({ otherDisplayName: local.targetDisplayName || '用户', otherUserId: local.targetUserId || '', otherAvatarUrl: local.targetAvatarUrl || '', canSend: local.canSendMessage !== false, }); } else { appStore.toastError(err instanceof Error ? err.message : '加载失败'); } } }, _syncFromStore(state: any) { const cid = this.data.conversationId; const rawList: Message[] = state.messagesByConversation?.[cid] || []; const hasMore = state.hasMoreByConversation?.[cid] !== false; const loading = state.conversationsLoading; const messages = this._formatMessageList(rawList); const patch: Partial = { messages, hasMore }; if (loading !== this.data.loading) patch.loading = loading; this.setData(patch); if (this._loadingMore && this._preserveMsgId) { const idx = messages.findIndex((m) => m.messageId === this._preserveMsgId); if (idx >= 0) this.setData({ scrollIntoView: `msg-${idx}` }); this._loadingMore = false; this._preserveMsgId = null; } else { this._scrollToLast(messages); } }, _formatMessageList(list: Message[]) { const otherAvatar = this.data.otherAvatarUrl; const result: Array = []; let lastTs = 0; for (let i = 0; i < list.length; i++) { const m = list[i]; const ts = (m as any).sentAt || 0; const showTime = i === 0 || (ts && ts - lastTs > TIME_GAP_MS); result.push({ ...m, showTime, sentAtText: showTime ? this._formatTime(ts) : '', showAvatar: m.bubbleType === 'other', avatarUrl: m.bubbleType === 'other' ? otherAvatar : '', }); lastTs = ts; } return result; }, _formatTime(ts: number | string): string { // 复用 utils/datetime,避免与会话列表各写一份时间格式化 return formatMessageTime(ts as any); }, _scrollToLast(list: typeof this.data.messages) { if (!list.length) return; const id = `msg-${list.length - 1}`; if (id !== this.data.scrollIntoView) this.setData({ scrollIntoView: id }); }, onBack() { const pages = getCurrentPages(); if (pages.length > 1) wx.navigateBack(); else wx.switchTab({ url: ROUTE.TAB.MESSAGES }); }, onInput(e: any) { this.setData({ inputText: (e as any).detail.value }); }, async onSend() { const { inputText, conversationId, sending, canSend } = this.data; if (!canSend || !inputText.trim() || sending) return; this.setData({ sending: true, inputText: '' }); try { // @ts-ignore await messageStore.sendMessage(conversationId, inputText.trim()); } catch (err: unknown) { const code = (err as any)?.code; if (code === BUSINESS_ERROR_CODE.MATCH_REQUIRED) { appStore.toast('双方互相喜欢后才能开始聊天'); } else { appStore.toastError(err instanceof Error ? err.message : '发送失败'); } } finally { this.setData({ sending: false }); } }, onRetrySend(e: any) { const messageId = (e.currentTarget.dataset as any).msgId as string | undefined; if (!messageId) return; // @ts-ignore messageStore.retrySend(this.data.conversationId, messageId).catch((err: unknown) => { appStore.toastError(err instanceof Error ? err.message : '重发失败'); }); }, async onScrollToUpper() { const { conversationId, hasMore, loadingMore } = this.data; if (!hasMore || loadingMore) return; const first = this.data.messages[0]; this._loadingMore = true; this._preserveMsgId = first?.messageId || null; this.setData({ loadingMore: true }); try { // @ts-ignore await messageStore.loadMoreMessages(conversationId); } catch (err: unknown) { appStore.toastError(err instanceof Error ? err.message : '加载失败'); this._loadingMore = false; this._preserveMsgId = null; } finally { this.setData({ loadingMore: false }); } }, onMore() { const { otherUserId } = this.data; if (!otherUserId) { appStore.toast('暂无更多信息'); return; } wx.showActionSheet({ itemList: ['查看 Ta 的资料', '举报 Ta', '拉黑 Ta'], success: (res) => { if (res.tapIndex === 0) wx.navigateTo({ url: to.detail(otherUserId) }); else if (res.tapIndex === 1) wx.navigateTo({ url: to.report(otherUserId) }); else if (res.tapIndex === 2) this.setData({ showDangerDialog: true, dangerAction: DANGER_ACTION.BLOCK }); }, }); }, async onConfirmDanger(e: WechatMiniprogram.CustomEvent) { const action = (e.detail as any)?.action as string | undefined; const { otherUserId } = this.data; this.setData({ showDangerDialog: false }); if (action !== DANGER_ACTION.BLOCK) return; try { appStore.showLoading('处理中…'); await blockUser(otherUserId, '用户主动拉黑'); appStore.hideLoading(); this.setData({ canSend: false }); appStore.toastSuccess('已拉黑'); } catch (err: unknown) { appStore.hideLoading(); appStore.toastError(err instanceof Error ? err.message : '操作失败'); } }, onCancelDanger() { this.setData({ showDangerDialog: false }); }, _unsubConn: (() => {}) as () => void, _unsubList: (() => {}) as () => void, _unsubRealtime: (() => {}) as () => void, _loadingMore: false, _preserveMsgId: null as string | null, });