# 前端聊天页面 - 学习笔记(通俗版) ## 一、核心概念理解 ### 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. **多看报错**:出错时看报错信息,学会查问题 --- **记住**:编程就像学开车,一开始慢,多练习就熟了!🚀