# chat 页面数据来源排查 > 排查日期:2026-03-18 > 页面路径:pages/chat/chat ## 概览 | 分类 | 数量 | 说明 | |------|------|------| | A. Mock 数据 | 4 | 来自 `mock-data.ts` 或页面内联 mock,联调时需替换为 API | | B. 硬编码数据 | 3 | 直接写死在 data 或方法中的值 | | C. 已对接 API | 0 | 当前页面无任何真实 API 调用 | | D. 前端计算/派生数据 | 6 | 由工具函数或页面逻辑计算得出 | | E. 路由参数 | 1 | 从 `onLoad(options)` 获取 | | F. WXML 硬编码文案 | 8 | 模板中直接写死的中文文案 | --- ## 一、Mock 数据 ### A1. `mockChatMessages` — 历史消息列表 | 项目 | 内容 | |------|------| | 来源 | `import { mockChatMessages } from '../../utils/mock-data'` | | 类型 | `ChatMessage[]` | | 使用位置 | `loadMessages()` → `enrichMessages(mockChatMessages)` → `this.setData({ messages })` | | 涉及字段 | `id`, `role`, `content`, `timestamp`, `referenceCard`(含 `type`, `title`, `summary`, `data`) | | 联调替换 | 需替换为 **AI 对话历史 API**(如 `GET /api/chat/messages?customerId=xxx`),返回分页消息列表 | | 当前 mock 条数 | 6 条(`msg-001` ~ `msg-006`) | ### A2. `mockAIReplies` — AI 回复模板(页面内联 mock) | 项目 | 内容 | |------|------| | 来源 | `chat.ts` 顶部常量 `const mockAIReplies = [...]` | | 类型 | `string[]`(3 条固定回复文案) | | 使用位置 | `triggerAIReply()` → `mockAIReplies[this._msgCounter % mockAIReplies.length]` | | 联调替换 | 需替换为 **AI 对话流式接口**(如 `POST /api/chat/send`,SSE/WebSocket 流式返回) | | 当前值 | `['根据数据分析,这位客户近期消费频次有所下降...', '好的,我已经记录了你的需求...', '这位客户偏好中式台球...']` | ### A3. `simulateStreamOutput` — 模拟流式输出 | 项目 | 内容 | |------|------| | 来源 | `import { simulateStreamOutput } from '../../utils/chat'` | | 类型 | 工具函数,逐字 `setTimeout(tick, 50)` 模拟打字效果 | | 使用位置 | `triggerAIReply()` 中调用,将 mock 回复逐字输出 | | 联调替换 | 替换为真实 SSE/WebSocket 流式读取逻辑,`callback` 接口可复用 | ### A4. `referenceCard` — 页面顶部引用卡片(条件构造) | 项目 | 内容 | |------|------| | 来源 | `loadMessages()` 中根据 `customerId` 内联构造 | | 代码 | `{ title: '客户详情', summary: \`正在查看客户 ${customerId} 的相关信息\` }` | | 联调替换 | 需从 **客户详情 API** 获取客户名称等信息填充,或由跳转页面通过路由参数传入 | --- ## 二、硬编码数据 ### B1. `statusBarHeight` 回退值 | 项目 | 内容 | |------|------| | 字段 | `statusBarHeight` | | 位置 | `onLoad()` → `sysInfo.statusBarHeight \|\| 44` | | 当前值 | `44`(回退默认值) | | 应获取自 | `wx.getWindowInfo().statusBarHeight`(已获取,44 仅为兜底) | | 风险等级 | **低** — 兜底值合理,不同机型差异可忽略 | ### B2. `loadMessages` 延迟时间 | 项目 | 内容 | |------|------| | 字段 | `setTimeout(() => { ... }, 500)` | | 位置 | `loadMessages()` 方法 | | 当前值 | `500`ms 模拟网络延迟 | | 联调处理 | 替换为真实 API 调用后移除 `setTimeout`,改用 API 响应回调 | | 风险等级 | **中** — 联调时必须移除,否则造成不必要的 500ms 延迟 | ### B3. `triggerAIReply` 延迟时间 | 项目 | 内容 | |------|------| | 字段 | `setTimeout(() => { this.triggerAIReply() }, 300)` | | 位置 | `onSendMessage()` 末尾 | | 当前值 | `300`ms 延迟触发 AI 回复 | | 联调处理 | 替换为发送消息 API 成功回调后触发流式接收 | | 风险等级 | **中** — 联调时需重构为 API 驱动的流程 | --- ## 三、已对接 API **无。** 当前页面 0 个真实 API 调用。 > 注:项目中已存在 `utils/request.ts`(封装了 `wx.request`),联调时可直接使用。 --- ## 四、前端计算/派生数据 ### D1. `enrichMessages()` 派生字段 对每条消息计算以下展示字段: | 派生字段 | 计算逻辑 | 来源函数 | |----------|----------|----------| | `timeLabel` | 相对时间文案(刚刚/N分钟前/MM-DD) | `formatRelativeTime(m.timestamp)` | | `imTimeLabel` | IM 时间格式(HH:mm / MM-DD HH:mm) | `formatIMTime(m.timestamp)` | | `showTimeDivider` | 是否显示时间分割线(首条必显示,间隔≥5分钟显示) | `shouldShowTimeDivider(prev, curr)` | | `referenceCard.dataList` | 将 `Record` 转为 `{key,value}[]` 供 `wx:for` | `toDataList(m.referenceCard.data)` | ### D2. 用户发送消息时的派生字段 | 派生字段 | 计算逻辑 | |----------|----------| | `userMsg.timeLabel` | 固定为 `'刚刚'` | | `userMsg.imTimeLabel` | `formatIMTime(new Date().toISOString())` | | `userMsg.showTimeDivider` | `shouldShowTimeDivider(prevTs, now)` | | `userMsg.id` | `msg-user-${this._msgCounter}`(自增计数器) | | `userMsg.timestamp` | `new Date().toISOString()`(客户端当前时间) | ### D3. AI 回复消息的派生字段 同 D2 逻辑,`id` 格式为 `msg-ai-${this._msgCounter}`。 ### D4. `pageState` 状态机 | 状态值 | 触发条件 | |--------|----------| | `'loading'` | `loadMessages()` 开始时 | | `'empty'` | 消息列表为空且无引用卡片 | | `'normal'` | 消息加载成功且有内容 | | `'error'` | `loadMessages()` 捕获异常 | ### D5. `isStreaming` / `streamingContent` | 字段 | 说明 | |------|------| | `isStreaming` | AI 流式回复进行中标志,控制输入框禁用和打字指示器 | | `streamingContent` | 流式输出的当前内容片段 | ### D6. `scrollToId` 滚动锚点,通过 `scrollToBottom()` 方法设置为 `'scroll-bottom'`,驱动 `scroll-view` 滚动到底部。 --- ## 五、路由参数 ### E1. `customerId` | 项目 | 内容 | |------|------| | 获取方式 | `onLoad(options)` → `options?.customerId \|\| ''` | | 用途 | 1. 传入 `loadMessages(customerId)` 加载对应客户的对话;2. 决定是否显示页面顶部引用卡片 | | 当前状态 | 仅用于 mock 逻辑判断,未传入任何 API | | 联调需求 | 作为 API 请求参数传入消息列表接口 | --- ## 六、WXML 硬编码文案 | 编号 | 文案 | 位置 | 说明 | |------|------|------|------| | F1 | `加载中...` | 加载态 `g-toast-loading-text` | 通用加载文案,可保留 | | F2 | `加载失败,请重试` | 错误态 `error-text` | 通用错误文案,可保留 | | F3 | `重新加载` | 错误态 `retry-btn-text` | 按钮文案,可保留 | | F4 | `引用内容` | 引用卡片 `reference-tag` | 标签文案,可保留 | | F5 | `你好,我是 AI 助手` | 空对话提示 `empty-text` | 欢迎语,建议后续从配置获取 | | F6 | `有什么可以帮你的?` | 空对话提示 `empty-sub` | 副标题,建议后续从配置获取 | | F7 | `输入消息...` | 输入框 `placeholder` | 占位文案,可保留 | | F8 | `AI 助手` | `chat.json` → `navigationBarTitleText` | 导航栏标题,可保留 | --- ## 七、联调 TODO ### 优先级 P0(阻塞联调) - [ ] **替换消息列表加载**:`loadMessages()` 中 `mockChatMessages` → 调用 `GET /api/chat/messages` 接口 - 入参:`customerId`(可选)、分页参数 - 出参:`ChatMessage[]` - 移除 `setTimeout(..., 500)` 模拟延迟 - [ ] **替换 AI 回复流程**:`triggerAIReply()` 中 `mockAIReplies` + `simulateStreamOutput` → 调用 `POST /api/chat/send` 流式接口 - 入参:`content`(用户消息)、`customerId`、`conversationId` - 出参:SSE/WebSocket 流式文本 - 保留 `isStreaming` 状态控制和逐字渲染逻辑 - [ ] **替换引用卡片数据**:`loadMessages()` 中内联构造的 `referenceCard` → 从客户详情 API 或路由参数获取真实客户信息 ### 优先级 P1(联调后优化) - [ ] **消息发送 API**:`onSendMessage()` 中用户消息需先通过 API 持久化,再触发 AI 回复 - [ ] **对话会话管理**:当前无 `conversationId` 概念,需增加会话创建/恢复逻辑 - [ ] **消息 ID 生成**:当前使用前端自增计数器 `_msgCounter`,联调后应使用服务端返回的 ID ### 优先级 P2(体验优化) - [ ] **错误重试**:`onRetry()` 当前仅重新调用 `loadMessages()`,需增加 toast 提示 - [ ] **空对话欢迎语**:F5/F6 硬编码文案考虑从后端配置获取 - [ ] **移除 `simulateStreamOutput`**:联调完成后,`utils/chat.ts` 中的模拟函数可删除(保留 `simulateStreamOutputSync` 用于测试) - [ ] **移除 `mockAIReplies`**:页面内联 mock 常量清理 - [ ] **移除 `mock-data.ts` 中 `mockChatMessages` 引用**:确认其他页面不再使用后,清理 `ChatMessage` 相关 mock ### 依赖确认 | 依赖项 | 状态 | 说明 | |--------|------|------| | `utils/request.ts` | ✅ 已存在 | 封装了 `wx.request`,可直接使用 | | AI 对话 API(后端) | ❌ 待确认 | 需确认接口设计:REST vs WebSocket、流式协议 | | 客户详情 API | ❌ 待确认 | 引用卡片需要客户基本信息 | | 对话会话 API | ❌ 待确认 | 会话创建、历史会话列表 |