Files
danyang-match/miniapp/pages/chat/chat.ts
T
WorkBuddy c0298c27a5 fix(miniapp): 小程序界面全量中文化,消除英文/非中文枚举码上屏
- 新增 miniapp/constants/profileLabels.js 作为「枚举码→中文简体」唯一字典
  (婚姻/子女/学历/职业/吸烟饮酒/择偶子女/红娘状态),覆盖表单值与种子
  实际值两套并集,保证详情页无论后端返回哪种码都显示中文
- 修复「择偶条件-子女」下拉英文选项(any/accept/reject→不限/可接受/不接受),
  改用 value/label 数组 + range-key="label"
- 修复 recommend-card / user-card / profile-detail / profile-confirm 直接渲染
  后端枚举码(educationCode/maritalStatus/occupationCode…)的问题,改由
  observers 派生中文 label 上屏
- 清理残留英文文案:Required 条件→必须符合的条件、喜欢TA/向Ta→喜欢对方/向对方、
  U51 服务预约→服务预约、U64 公益服务记录→公益服务记录
- 修复 7 个英文窗口标题与 3 个英文占位页正文
- matchmaker 历史服务状态消除原始码兜底,统一走字典
- 加固 scripts/audit-miniapp-language.mjs(A 整行先剥离 mustache、B 排除
  bind:action 处理器、F 窗口标题、JPG/PNG 入白名单)
- 新增 ui-tests/miniapp-language-contracts.mjs 语种守卫(复用审计脚本,断言 0 命中),
  UI 契约门禁 MIN_GUARDS 21→22;已做负向注入验证判别力
2026-09-23 11:19:17 +08:00

281 lines
9.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 聊天详情页 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<Message & { showTime: boolean; sentAtText: string; showAvatar: boolean; avatarUrl: string }>;
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<string, string | undefined>) {
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<ChatData> = { 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<Message & { showTime: boolean; sentAtText: string; showAvatar: boolean; avatarUrl: string }> = [];
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: ['查看对方的资料', '举报对方', '拉黑对方'],
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,
});