# 前端聊天页面 - 学习笔记(通俗版)
## 一、核心概念理解
### 1. useState(数据盒子)
**类比**:就像一个小盒子,可以存东西,也可以拿出来
- `const [数据, 改数据的函数] = useState(初始值)`
- **例子**:`const [input, setInput] = useState("")`
- `input` = 盒子里现在装的是什么(当前值)
- `setInput` = 改变盒子里东西的函数
- `""` = 盒子一开始是空的
**为什么需要?**
- 数据变了,页面自动更新(不用手动刷新)
### 2. useEffect(监听器)
**类比**:就像"当...的时候,执行..."
- `useEffect(() => { 做什么 }, [监听谁])`
- `[]` = 空数组,意思是"只在页面第一次加载时执行一次"
- `[某个数据]` = 当这个数据改变时执行
**为什么需要?**
- 页面加载时自动执行某些操作(比如拉取数据)
- 当数据改变时,自动执行某些操作(比如自动滚动)
### 3. useRef(指针/书签)
**类比**:就像书签,可以指向页面上某个元素
- `const ref = useRef(null)`
- `ref.current` = 指向的那个元素
**为什么需要?**
- 实现自动滚动(找到消息列表底部,滚动到那里)
### 4. async/await(等待)
**类比**:就像"等外卖"
- `async` = 这是一个需要等待的函数
- `await` = 在这里等,等到了才继续
- `fetch` = 发送网络请求(就像点外卖)
**为什么需要?**
- 网络请求需要时间,不能立即返回,所以要等
### 5. useRouter(路由导航)
**类比**:就像地图导航,告诉浏览器"我要去哪里"
- `const router = useRouter()` = 获取路由对象
- `router.push("/路径")` = 跳转到指定页面
- `router.replace("/路径")` = 跳转并替换历史记录(不能返回)
**为什么需要?**
- 登录成功后需要跳转到其他页面
- 未登录时需要跳转到登录页
### 6. WebSocket(实时通信)
**类比**:就像对讲机,可以实时双向通信
- `WSClient` = WebSocket 客户端类,封装连接逻辑
- `wsClient.connect()` = 建立连接
- `wsClient.disconnect()` = 断开连接
- `onMessage` = 收到消息时的回调函数
**为什么需要?**
- HTTP 只能客户端主动请求,不能服务器主动推送
- WebSocket 可以服务器主动推送消息,实现实时通信
- 新消息自动显示,无需手动刷新
### 7. 多状态管理(复杂组件)
**类比**:就像管理多个盒子,每个盒子存不同的数据
- 对话列表状态:`const [conversations, setConversations] = useState([])`
- 选中对话状态:`const [selectedConversationId, setSelectedConversationId] = useState(null)`
- 消息列表状态:`const [messages, setMessages] = useState([])`
- 输入框状态:`const [input, setInput] = useState("")`
**为什么需要?**
- 复杂页面需要管理多个数据
- 每个数据独立管理,互不干扰
- 数据改变时,只更新相关的 UI 部分
## 二、常用英文单词记忆
| 英文 | 中文意思 | 记忆技巧 |
|------|---------|---------|
| **state** | 状态 | 记住:state = 状态(盒子里的数据状态) |
| **effect** | 效果/影响 | 记住:effect = 当...的时候产生的效果 |
| **ref** | 引用/指针 | 记住:ref = reference(引用),指向某个东西 |
| **async** | 异步的 | 记住:async = asynchronized(异步),需要等待 |
| **await** | 等待 | 记住:await = wait(等待) |
| **fetch** | 获取 | 记住:fetch = 去拿(去后端拿数据) |
| **try** | 尝试 | 记住:try = 尝试(试试看能不能成功) |
| **catch** | 抓住 | 记住:catch = 抓住(如果出错了,抓住错误) |
| **finally** | 最终 | 记住:finally = 最终(最后一定要做的事) |
| **preventDefault** | 阻止默认 | 记住:prevent(阻止)+ default(默认)= 阻止默认行为 |
| **router** | 路由 | 记住:router = 路由器(导航到不同页面) |
| **push** | 推送/跳转 | 记住:push = 推(推送到新页面) |
| **replace** | 替换 | 记住:replace = 替换(替换当前页面) |
| **params** | 参数 | 记住:params = parameters(参数),URL 中的参数 |
| **websocket** | WebSocket | 记住:websocket = 实时通信协议(双向通信) |
| **client** | 客户端 | 记住:client = 客户端(WebSocket 客户端) |
| **connect** | 连接 | 记住:connect = 连接(建立 WebSocket 连接) |
| **disconnect** | 断开 | 记住:disconnect = 断开(断开 WebSocket 连接) |
| **callback** | 回调 | 记住:callback = 回调(收到消息时执行的函数) |
| **layout** | 布局 | 记住:layout = 布局(页面布局结构) |
| **dashboard** | 仪表盘 | 记住:dashboard = 仪表盘(工作台页面) |
| **hook** | 钩子 | 记住:hook = React 自定义逻辑的钩子函数 |
| **service** | 服务 | 记住:service = 统一封装接口请求的模块 |
| **module** | 模块 | 记住:module = 一组功能组成的模块化单元 |
## 三、代码执行流程(就像讲故事)
### 访客端流程:
1. **页面加载**
- 检查浏览器里有没有访客ID
- 如果没有,生成一个新的ID存起来
2. **有了访客ID后**
- 打电话给后端:"给我一个对话ID"
- 后端回复:"你的对话ID是123"
3. **有了对话ID后**
- 自动拉取这个对话的所有消息
- 显示在页面上
4. **用户发送消息**
- 用户点击"发送"按钮
- 把消息内容发送给后端
- 后端保存成功
- 重新拉取消息(能看到刚发的)
- 自动滚动到底部
5. **每次消息更新**
- 自动滚动到底部(让用户看到最新消息)
### 客服端流程(旧版,已废弃):
1. **登录页面加载**
- 用户输入用户名和密码
- 点击"登录"按钮
2. **登录请求**
- 发送登录请求到后端
- 后端验证用户名和密码
- 返回用户信息(user_id、username、role)
3. **登录成功**
- 保存用户信息到 localStorage
- 跳转到对话列表页面
4. **对话列表页面**
- 检查是否已登录(检查 localStorage)
- 未登录则跳转到登录页
- 已登录则拉取所有未关闭的对话
- 显示对话列表
5. **进入聊天页面**
- 点击对话,跳转到 `/agent/chat/[conversationId]`
- 从 URL 参数获取对话ID
- 拉取该对话的所有消息
- 客服消息显示在右侧,访客消息显示在左侧
6. **发送消息**
- 客服输入消息,点击"发送"
- 发送时设置 `sender_is_agent: true`
- 消息显示在右侧(蓝色气泡)
### 客服端流程(新版,四栏布局):
1. **登录页面加载**
- 用户输入用户名和密码
- 点击"登录"按钮
2. **登录请求**
- 发送登录请求到后端
- 后端验证用户名和密码
- 返回用户信息(user_id、username、role)
3. **登录成功**
- 保存用户信息到 localStorage
- 跳转到 `/agent/dashboard`(四栏布局工作台)
4. **工作台页面加载**
- 检查是否已登录(检查 localStorage)
- 未登录则跳转到登录页
- 已登录则拉取所有未关闭的对话
- 显示在左侧对话列表栏
5. **选择对话**
- 点击左侧对话列表中的某个对话
- 更新 `selectedConversationId` 状态
- 自动拉取该对话的所有消息
- 建立 WebSocket 连接,接收实时消息
- 中间栏显示聊天内容
- 右侧栏显示访客详情
6. **发送消息**
- 在中间栏输入框输入消息
- 点击"发送"按钮
- 发送时设置 `sender_is_agent: true`
- 消息通过 WebSocket 实时显示在右侧(蓝色气泡)
7. **实时接收消息**
- WebSocket 接收到新消息
- 自动更新消息列表
- 自动滚动到底部
- 无需手动刷新
## 四、模块化拆分(2025-11)
> 拆分后的代码更像乐高积木,每一块负责自己的事情,组合起来就是完整的客服工作台。
- **页面层(app/agent/dashboard/page.tsx)**
- 只做一件事:渲染 ``
- 没有业务逻辑,后续做 SSR / Route Handlers 时更轻松
- **组件层(components/dashboard/)**
- `DashboardShell`:整合左中右三栏 + 顶部导航
- `NavigationSidebar / ConversationSidebar / MessageList / VisitorDetailPanel`:界面分块清晰,可复用
- `VisitorDetailPanel` 内置联系人信息编辑弹窗,点击“+ Add / 编辑”即可修改邮箱、电话、备注
- 样式问题在各自组件内部解决,互不影响
- `MessageList` 通过 `currentUserIsAgent` 参数兼容客服/访客视角
- **Hook 层(features/agent/hooks/)**
- `useAuth`:登录信息获取 + 退出登录
- `useConversations`:对话列表、防抖搜索、未读数更新
- `useMessages`:消息拉取、已读状态、WebSocket 回调,新增 `updateContactInfo` 用于保存邮箱/电话/备注
- `useWebSocket`:封装连接/断开/错误处理
- **Service 层(features/agent/services/)**
- 所有 `fetch` 请求集中在这里,例如 `conversationApi.ts`、`messageApi.ts`、`authApi.ts`
- 新增 `updateConversationContact` 方法,调用 `PUT /conversations/:id/contact` 更新访客联系信息
- 后续接入 React Query / SWR 时,只需要在这里改
- **工具层(utils/)**
- `format.ts`:统一时间和消息预览格式
- `highlight.tsx`:关键词高亮组件化
- `storage.ts`:localStorage 读写统一封装
> 小结:页面调用 Hook,Hook 使用 Service,Service 请求后端;UI 部分由组件层独立负责。以后要换样式或替换数据源,都有明确位置可以下手。
### 目录与关键文件(2025-11)
| 层级 | 目录 / 文件 | 作用说明 |
|------|-------------|----------|
| 页面入口 | `app/page.tsx` | 客服登录页,登录成功后跳转工作台 |
| 页面入口 | `app/agent/dashboard/page.tsx` | 工作台入口,渲染 `DashboardShell` |
| 页面入口 | `app/agent/chat/[conversationId]/page.tsx` | 旧单聊页面,复用新版组件与 Hook |
| 页面入口 | `app/chat/page.tsx` | 访客端聊天页面,复用统一组件/服务 |
| 组件层 | `components/dashboard/*` | `DashboardShell`、导航栏、会话列表、消息列表、访客详情、输入框等 UI 组件 |
| Hook 层 | `features/agent/hooks/useAuth` | 处理登录态:读取/清理 localStorage,提供退出方法 |
| Hook 层 | `features/agent/hooks/useConversations` | 统一管理会话列表、搜索、防抖、排序、选中会话 |
| Hook 层 | `features/agent/hooks/useMessages` | 统一管理消息、已读状态、详情数据、WebSocket 回调 |
| Hook 层 | `features/agent/hooks/useWebSocket` | 对 `WSClient` 的通用封装,负责连接/断开 |
| Service | `features/agent/services/conversationApi` | 会话相关接口封装:列表、搜索、详情、更新访客联系信息 |
| Service | `features/agent/services/messageApi` | 消息接口封装:拉取、发送、已读 |
| Service | `features/agent/services/authApi` | 登录态接口(目前仅登出) |
| Service | `features/visitor/services/conversationApi` | 访客端对话初始化接口(收集 UA、语言等) |
| 类型定义 | `features/agent/types.ts` | 会话、消息、用户、WebSocket 负载等公共类型 |
| 工具 | `utils/format.ts` | 时间格式化、消息预览截断 |
| 工具 | `utils/highlight.tsx` | 关键词高亮渲染(返回 ``) |
| 工具 | `utils/storage.ts` | localStorage 读写封装(获取/设置/清理客服账号) |
| 工具 | `lib/websocket.ts` | `WSClient`:负责连接、自动重连、消息广播 |
> 访客与客服共用同一套消息组件/服务:通过 `currentUserIsAgent`、`senderIsAgent` 等参数切换左右气泡与已读对勾。
## 五、为什么这样写?
### 为什么用 useState?
- 如果不用:数据变了,页面不会自动更新,要手动刷新
- 用了:数据一变,页面自动刷新
### 为什么用 useEffect?
- 如果不用:需要手动点击按钮才能拉取数据
- 用了:页面加载时自动拉取数据
### 为什么用 async/await?
- 如果不用:代码不会等网络请求完成,可能会出错
- 用了:等网络请求完成再继续,保证顺序
### 为什么用 try/catch?
- 如果不用:网络断了,程序会崩溃
- 用了:即使出错,也能显示错误信息,程序不崩溃
## 五、记忆口诀
1. **useState**:数据盒子,存和改
2. **useEffect**:当...的时候,做...
3. **useRef**:书签指针,找元素
4. **async/await**:等待外卖,别着急
5. **try/catch**:试试看,错了别慌
6. **useRouter**:地图导航,跳页面
7. **localStorage**:浏览器存储,存数据
8. **URL参数**:从路径拿数据,用 useParams
9. **WebSocket**:对讲机,实时通信
10. **多状态**:多个盒子,各管各的
11. **布局**:四栏布局,各司其职
## 六、常见错误和解决
1. **忘记写 await**
- 错误:`fetch(...)` 后面直接用 `.json()`
- 正确:`await fetch(...)` 然后 `await res.json()`
2. **useEffect 依赖写错**
- 错误:`[]` 写成了 `[data]`,导致无限循环
- 正确:想清楚"当谁改变时才执行"
3. **忘记检查数据是否存在**
- 错误:直接用 `data.id`,如果 data 是 null 就报错
- 正确:先检查 `if (data)` 再使用
4. **忘记检查登录状态**
- 错误:直接访问需要登录的页面,没有检查是否已登录
- 正确:在页面加载时检查 localStorage 中是否有登录信息
5. **路由跳转问题**
- 错误:使用 `window.location.href` 跳转(会刷新页面)
- 正确:使用 `router.push()` 跳转(不会刷新,更流畅)
6. **WebSocket 连接问题**
- 错误:在组件每次渲染时都创建新连接(导致连接泄漏)
- 正确:在 `useEffect` 中创建连接,在清理函数中断开连接
- 注意:对话切换时,需要断开旧连接,建立新连接
7. **状态管理问题**
- 错误:把所有数据放在一个状态里(难以管理)
- 正确:按功能拆分状态(对话列表、选中对话、消息列表等)
- 注意:状态更新时,只更新相关的 UI 部分
8. **对话切换问题**
- 错误:切换对话时不清空消息列表(可能显示错误的消息)
- 正确:切换对话时先清空消息列表,再加载新对话的消息
## 七、练习建议
1. **多写注释**:每行代码都写注释,解释"为什么要这样写"
2. **画流程图**:把代码执行流程画出来,理清思路
3. **小步调试**:每次只改一小部分,测试看看效果
4. **多看报错**:出错时看报错信息,学会查问题
---
**记住**:编程就像学开车,一开始慢,多练习就熟了!🚀