mirror of
https://github.com/2930134478/AI-CS.git
synced 2026-06-15 08:45:41 +08:00
15 KiB
15 KiB
前端聊天页面 - 学习笔记(通俗版)
一、核心概念理解
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 = 一组功能组成的模块化单元 |
三、代码执行流程(就像讲故事)
访客端流程:
-
页面加载
- 检查浏览器里有没有访客ID
- 如果没有,生成一个新的ID存起来
-
有了访客ID后
- 打电话给后端:"给我一个对话ID"
- 后端回复:"你的对话ID是123"
-
有了对话ID后
- 自动拉取这个对话的所有消息
- 显示在页面上
-
用户发送消息
- 用户点击"发送"按钮
- 把消息内容发送给后端
- 后端保存成功
- 重新拉取消息(能看到刚发的)
- 自动滚动到底部
-
每次消息更新
- 自动滚动到底部(让用户看到最新消息)
客服端流程(旧版,已废弃):
-
登录页面加载
- 用户输入用户名和密码
- 点击"登录"按钮
-
登录请求
- 发送登录请求到后端
- 后端验证用户名和密码
- 返回用户信息(user_id、username、role)
-
登录成功
- 保存用户信息到 localStorage
- 跳转到对话列表页面
-
对话列表页面
- 检查是否已登录(检查 localStorage)
- 未登录则跳转到登录页
- 已登录则拉取所有未关闭的对话
- 显示对话列表
-
进入聊天页面
- 点击对话,跳转到
/agent/chat/[conversationId] - 从 URL 参数获取对话ID
- 拉取该对话的所有消息
- 客服消息显示在右侧,访客消息显示在左侧
- 点击对话,跳转到
-
发送消息
- 客服输入消息,点击"发送"
- 发送时设置
sender_is_agent: true - 消息显示在右侧(蓝色气泡)
客服端流程(新版,四栏布局):
-
登录页面加载
- 用户输入用户名和密码
- 点击"登录"按钮
-
登录请求
- 发送登录请求到后端
- 后端验证用户名和密码
- 返回用户信息(user_id、username、role)
-
登录成功
- 保存用户信息到 localStorage
- 跳转到
/agent/dashboard(四栏布局工作台)
-
工作台页面加载
- 检查是否已登录(检查 localStorage)
- 未登录则跳转到登录页
- 已登录则拉取所有未关闭的对话
- 显示在左侧对话列表栏
-
选择对话
- 点击左侧对话列表中的某个对话
- 更新
selectedConversationId状态 - 自动拉取该对话的所有消息
- 建立 WebSocket 连接,接收实时消息
- 中间栏显示聊天内容
- 右侧栏显示访客详情
-
发送消息
- 在中间栏输入框输入消息
- 点击"发送"按钮
- 发送时设置
sender_is_agent: true - 消息通过 WebSocket 实时显示在右侧(蓝色气泡)
-
实时接收消息
- WebSocket 接收到新消息
- 自动更新消息列表
- 自动滚动到底部
- 无需手动刷新
四、模块化拆分(2025-11)
拆分后的代码更像乐高积木,每一块负责自己的事情,组合起来就是完整的客服工作台。
-
页面层(app/agent/dashboard/page.tsx)
- 只做一件事:渲染
<DashboardShell /> - 没有业务逻辑,后续做 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 |
关键词高亮渲染(返回 <mark>) |
| 工具 | utils/storage.ts |
localStorage 读写封装(获取/设置/清理客服账号) |
| 工具 | lib/websocket.ts |
WSClient:负责连接、自动重连、消息广播 |
访客与客服共用同一套消息组件/服务:通过
currentUserIsAgent、senderIsAgent等参数切换左右气泡与已读对勾。
五、为什么这样写?
为什么用 useState?
- 如果不用:数据变了,页面不会自动更新,要手动刷新
- 用了:数据一变,页面自动刷新
为什么用 useEffect?
- 如果不用:需要手动点击按钮才能拉取数据
- 用了:页面加载时自动拉取数据
为什么用 async/await?
- 如果不用:代码不会等网络请求完成,可能会出错
- 用了:等网络请求完成再继续,保证顺序
为什么用 try/catch?
- 如果不用:网络断了,程序会崩溃
- 用了:即使出错,也能显示错误信息,程序不崩溃
五、记忆口诀
- useState:数据盒子,存和改
- useEffect:当...的时候,做...
- useRef:书签指针,找元素
- async/await:等待外卖,别着急
- try/catch:试试看,错了别慌
- useRouter:地图导航,跳页面
- localStorage:浏览器存储,存数据
- URL参数:从路径拿数据,用 useParams
- WebSocket:对讲机,实时通信
- 多状态:多个盒子,各管各的
- 布局:四栏布局,各司其职
六、常见错误和解决
-
忘记写 await
- 错误:
fetch(...)后面直接用.json() - 正确:
await fetch(...)然后await res.json()
- 错误:
-
useEffect 依赖写错
- 错误:
[]写成了[data],导致无限循环 - 正确:想清楚"当谁改变时才执行"
- 错误:
-
忘记检查数据是否存在
- 错误:直接用
data.id,如果 data 是 null 就报错 - 正确:先检查
if (data)再使用
- 错误:直接用
-
忘记检查登录状态
- 错误:直接访问需要登录的页面,没有检查是否已登录
- 正确:在页面加载时检查 localStorage 中是否有登录信息
-
路由跳转问题
- 错误:使用
window.location.href跳转(会刷新页面) - 正确:使用
router.push()跳转(不会刷新,更流畅)
- 错误:使用
-
WebSocket 连接问题
- 错误:在组件每次渲染时都创建新连接(导致连接泄漏)
- 正确:在
useEffect中创建连接,在清理函数中断开连接 - 注意:对话切换时,需要断开旧连接,建立新连接
-
状态管理问题
- 错误:把所有数据放在一个状态里(难以管理)
- 正确:按功能拆分状态(对话列表、选中对话、消息列表等)
- 注意:状态更新时,只更新相关的 UI 部分
-
对话切换问题
- 错误:切换对话时不清空消息列表(可能显示错误的消息)
- 正确:切换对话时先清空消息列表,再加载新对话的消息
七、练习建议
- 多写注释:每行代码都写注释,解释"为什么要这样写"
- 画流程图:把代码执行流程画出来,理清思路
- 小步调试:每次只改一小部分,测试看看效果
- 多看报错:出错时看报错信息,学会查问题
记住:编程就像学开车,一开始慢,多练习就熟了!🚀