// pages/chat/chat.ts — AI 对话页 import { mockChatMessages } from '../../utils/mock-data' import type { ChatMessage } from '../../utils/mock-data' import { simulateStreamOutput } from '../../utils/chat' import { formatRelativeTime, formatIMTime, shouldShowTimeDivider } from '../../utils/time' /** 将 referenceCard.data (Record) 转为数组供 WXML wx:for 渲染 */ function toDataList(data?: Record): Array<{ key: string; value: string }> { if (!data) return [] return Object.keys(data).map((k) => ({ key: k, value: data[k] })) } /** 为消息列表补充展示字段(时间分割线、IM时间、引用卡片)*/ function enrichMessages(msgs: ChatMessage[]) { return msgs.map((m, i) => ({ ...m, timeLabel: formatRelativeTime(m.timestamp), imTimeLabel: formatIMTime(m.timestamp), showTimeDivider: shouldShowTimeDivider( i === 0 ? null : msgs[i - 1].timestamp, m.timestamp, ), referenceCard: m.referenceCard ? { ...m.referenceCard, dataList: toDataList(m.referenceCard.data) } : undefined, })) } /** Mock AI 回复模板 */ const mockAIReplies = [ '根据数据分析,这位客户近期消费频次有所下降,建议安排一次主动回访,了解具体原因。', '好的,我已经记录了你的需求。下次回访时我会提醒你。', '这位客户偏好中式台球,最近对斯诺克也产生了兴趣,可以推荐相关课程。', ] Page({ data: { /** 页面状态 */ pageState: 'loading' as 'loading' | 'empty' | 'normal' | 'error', /** 状态栏高度 */ statusBarHeight: 0, /** 消息列表 */ messages: [] as Array } }>, /** 输入框内容 */ inputText: '', /** AI 正在流式回复 */ isStreaming: false, /** 流式输出中的内容 */ streamingContent: '', /** 滚动锚点 */ scrollToId: '', /** 页面顶部引用卡片(从其他页面跳转时) */ referenceCard: null as { title: string; summary: string } | null, /** 客户 ID */ customerId: '', /** 输入栏距底部距离(软键盘弹出时动态调整)*/ inputBarBottom: 0, }, /** 消息计数器,用于生成唯一 ID */ _msgCounter: 0, onLoad(options) { const sysInfo = wx.getWindowInfo() const customerId = options?.customerId || '' this.setData({ customerId, statusBarHeight: sysInfo.statusBarHeight || 44, }) this.loadMessages(customerId) }, /** 加载消息(Mock) */ loadMessages(customerId: string) { this.setData({ pageState: 'loading' }) try { setTimeout(() => { // TODO: 替换为真实 API 调用 const messages = enrichMessages(mockChatMessages) this._msgCounter = messages.length // 如果携带 customerId,显示引用卡片 const referenceCard = customerId ? { title: '客户详情', summary: `正在查看客户 ${customerId} 的相关信息` } : null const isEmpty = messages.length === 0 && !referenceCard this.setData({ pageState: isEmpty ? 'empty' : 'normal', messages, referenceCard, }) this.scrollToBottom() }, 500) } catch { this.setData({ pageState: 'error' }) } }, /** 返回上一页 */ onBack() { wx.navigateBack() }, /** 重试加载 */ onRetry() { this.loadMessages(this.data.customerId) }, /** 软键盘弹出:输入栏上移至键盘顶部 */ onInputFocus(e: WechatMiniprogram.InputFocus) { const keyboardHeight = e.detail.height || 0 this.setData({ inputBarBottom: keyboardHeight }) setTimeout(() => this.scrollToBottom(), 120) }, /** 软键盘收起:输入栏归位 */ onInputBlur() { this.setData({ inputBarBottom: 0 }) }, /** 输入框内容变化 */ onInputChange(e: WechatMiniprogram.Input) { this.setData({ inputText: e.detail.value }) }, /** 发送消息 */ onSendMessage() { const text = this.data.inputText.trim() if (!text || this.data.isStreaming) return this._msgCounter++ const now = new Date().toISOString() const prevMsgs = this.data.messages const prevTs = prevMsgs.length > 0 ? prevMsgs[prevMsgs.length - 1].timestamp : null const userMsg = { id: `msg-user-${this._msgCounter}`, role: 'user' as const, content: text, timestamp: now, timeLabel: '刚刚', imTimeLabel: formatIMTime(now), showTimeDivider: shouldShowTimeDivider(prevTs, now), } const messages = [...this.data.messages, userMsg] this.setData({ messages, inputText: '', pageState: 'normal', }) this.scrollToBottom() setTimeout(() => { this.triggerAIReply() }, 300) }, /** 触发 AI 流式回复 */ triggerAIReply() { this._msgCounter++ const aiMsgId = `msg-ai-${this._msgCounter}` const replyText = mockAIReplies[this._msgCounter % mockAIReplies.length] const aiNow = new Date().toISOString() const prevMsgs = this.data.messages const prevTs = prevMsgs.length > 0 ? prevMsgs[prevMsgs.length - 1].timestamp : null const aiMsg = { id: aiMsgId, role: 'assistant' as const, content: '', timestamp: aiNow, timeLabel: '刚刚', imTimeLabel: formatIMTime(aiNow), showTimeDivider: shouldShowTimeDivider(prevTs, aiNow), } const messages = [...this.data.messages, aiMsg] this.setData({ messages, isStreaming: true, streamingContent: '', }) this.scrollToBottom() const aiIndex = messages.length - 1 simulateStreamOutput(replyText, (partial: string) => { const key = `messages[${aiIndex}].content` this.setData({ [key]: partial, streamingContent: partial, }) this.scrollToBottom() }).then(() => { this.setData({ isStreaming: false, streamingContent: '', }) }) }, /** 滚动到底部 */ scrollToBottom() { setTimeout(() => { this.setData({ scrollToId: '' }) setTimeout(() => { this.setData({ scrollToId: 'scroll-bottom' }) }, 50) }, 50) }, })