引言
模块效果图

在现代旅游应用中,提供一个智能客服模块可以极大地提升用户体验。通过集成先进的AI技术,我们可以为用户提供实时的旅行建议、解答疑问以及个性化推荐等服务。本文将详细介绍如何使用 React 和 react-vant 构建一个简洁而强大的旅游智能客服模块,并展示如何与后端聊天API进行交互以实现自然语言处理功能。
代码设计与实现聊天界面
1. 完整代码
Pages/Trip/index.jsx
jsx
import {
useEffect,
useState,
} from 'react'
import useTitle from '@/hooks/useTitle'
import{
chat
} from '@/llm'
import styles from './trip.module.css'
import{
Input,
Button,
Loading,
Toast,
} from 'react-vant'
import {
ChatO,
UserO,
} from '@react-vant/icons'
const Trip = () => {
useTitle('旅游智能客服')
const [text,setText] = useState('')
const [isSending,setIsSending] = useState(false)
const [messages,setMessages] = useState([
{
id:1,
content:'hello~',
role:'user'
},
{
id:2,
content:'hello,I am your assistant ,I can help you with your travel',
role:'assistant'
}
])
const handleChat =async()=>{
if(text.trim() === '') {
Toast.info({
message:'内容不能为空'
})
return
}
setIsSending(true)
setText('')
setMessages((prev)=>{
return [
...prev,
{
role:'user',
content:text
}
]
})
const newMessage = await chat([{
role:'user',
content:text
}]);
setMessages((prev)=>{
return [
...prev,
newMessage.data
]
})
setIsSending(false)
}
return (
<div className='flex flex-col h-all'>
<div className={`flex-1 ${styles.chatArea}`}>
{
messages.map((msg,index)=>{
return (
<div
key={index} {/*由于底部导航栏没有顺序变化,所以直接用索引作为key*/}
className={
msg.role === 'user' ? styles.messageRight : styles.messageLeft
}
>
{
msg.role=='assistant'?
<ChatO/>:
<UserO/>
}
{msg.content}
</div>
)
})
}
</div>
<div className={`flex ${styles.inputArea}`}>
<Input
value={text}
onChange={(e)=>setText(e)}
placeholder='请输入消息'
className={`flex-1 ${styles.input}`}
/>
<Button
type='primary'
onClick={handleChat}
disabled={isSending}
>
发送
</Button>
</div>
{isSending && (<div className='fixed-loading'>{/*离开文档流 */}
<Loading type='ball'/>
</div>)}
</div>
)
}
export default Trip
llm/index.js
js
const DEEPSEEK_CHAT_API_URL = 'https://api.deepseek.com/chat/completions';
// 聊天Chat AI
export const chat = async (
messages,
api_url = DEEPSEEK_CHAT_API_URL,
api_key = import.meta.env.VITE_DEEPSEEK_API_KEY,
model = 'deepseek-chat'
) => {
try {
const response = await fetch(api_url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${api_key}`
},
body: JSON.stringify({
model,
messages,
stream: false,
})
})
const data = await response.json();
return {
code: 0,
data: {
role: 'assistant',
content: data.choices[0].message.content
}
}
} catch(err) {
return {
code: 0,
msg: '出错了...'
}
}
}
Pages/Trip/trip.modules.css
css
.chatArea {
overflow-y: auto;
padding: 12px;
background-color: #f7f8fa;
}
.inputArea {
padding: 16px;
border-top: 1px solid #ddd;
background: white;
}
.input {
margin-right:8px;
}
.messageLeft, .messageRight{
max-width: 70%;
padding: 8px 12px;
margin: 12px 0;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.messageLeft {
background-color: #bcd3ef;
}
.messageRight {
background-color: #6eb16b;
margin-left: 30%;
}
2. 界面布局
聊天界面由两大部分组成:消息显示区 和输入区。消息显示区负责展示用户和助手之间的对话记录;输入区则允许用户输入新的消息并发送给服务器。
jsx
<div className='flex flex-col h-all'>
<div className={`flex-1 ${styles.chatArea}`}>
{/* 消息列表 */}
</div>
<div className={`flex ${styles.inputArea}`}>
{/* 输入框和发送按钮 */}
</div>
</div>
2.1 样式定制
为了使聊天界面更加美观,我们定义了一些基本的CSS样式。使用模块化css。
css
.chatArea {
overflow-y: auto;
padding: 12px;
background-color: #f7f8fa;
}
.messageLeft, .messageRight{
max-width: 70%;
padding: 8px 12px;
margin: 12px 0;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.messageLeft {
background-color: #bcd3ef;
}
.messageRight {
background-color: #6eb16b;
margin-left: 30%;
}
3. 数据驱动与逻辑处理
3.1 状态管理
使用 useState
钩子来管理聊天记录的状态。每当有新消息时,更新消息数组并将新消息添加到列表中。
jsx
const [text,setText] = useState('') // 用户聊天框输入的信息
const [isSending,setIsSending] = useState(false) // 是否已发送
const [messages,setMessages] = useState([ // 聊天记录
{
id:1,
content:'hello~',
role:'user' // 用户
},
{
id:2,
content:'hello,I am your assistant ,I can help you with your travel',
role:'assistant' // AI助手
}
])
这里有两个角色:user
用户和assistant
助手,我们需要判断一下角色并决定将其的对话框展示在哪边(左右)。
jsx
messages.map((msg,index)=>{
return (
<div
key={index}
className={
msg.role === 'user' ? styles.messageRight : styles.messageLeft
}
>
{
msg.role=='assistant'?
<ChatO/>:
<UserO/>
}
{msg.content}
</div>
)
})
在类名中使用三元运算符:msg.role === 'user' ? styles.messageRight : styles.messageLeft
来决定显示在哪侧。并用 msg.role=='assistant'?<ChatO/>:<UserO/>
显示对应的图标。
css
// trip.module.css
.messageLeft, .messageRight{
max-width: 70%;
padding: 8px 12px;
margin: 12px 0;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.messageLeft {
background-color: #bcd3ef;
}
.messageRight {
background-color: #6eb16b;
margin-left: 30%;
}
3.2 发送消息
当用户点击"发送 "按钮时,调用 handleChat
函数处理消息发送逻辑。包括验证输入内容是否为空、设置发送状态、更新本地消息列表以及调用聊天API获取回复。
jsx
const handleChat =async()=>{
if(text.trim() === '') {
Toast.info({
message:'内容不能为空'
})
return
}
setIsSending(true) // 已发送
setText('') // 清空输入框,提升用户体验
setMessages((prev)=>{ // 更新用户提问
return [
...prev,
{
role:'user',
content:text
}
]
})
const newMessage = await chat([{ // 更新AI客服回答
role:'user', // 切记传的是数组!!
content:text
}]);
setMessages((prev)=>{
return [
...prev,
newMessage.data
]
})
setIsSending(false)// 改为未发送
}
3.2.1 bug:闭包陷阱
在写项目的过程中,我一开始写的是这样的代码:
jsx
const handleChat = async () => {
if (text.trim() === "") {
Toast.info({ message: '内容不能为空' });
return;
}
setIsSending(true);
setText('')
// ❌ 闭包陷阱:捕获当前作用域的旧 messages 值
setMessages([...messages, { role: 'user', content: text }]);
const newMessage = await chat([{ role: 'user', content: text }]);
// ❌ 再次使用旧的 messages 值,导致状态更新异常
setMessages([...messages, newMessage.data]);
setIsSending(false);
};
这样导致用户提问后,当助手回答出现后,用户的提问就消失不见了。
如图:
什么是"闭包陷阱"?
在 React 中,函数组件每次渲染都会创建一个新的函数实例 ,包括
handleChat
。如果你在异步操作中使用了状态变量(如messages
),而没有使用setState
的函数式更新方式,那么拿到的可能是一个旧的状态快照(snapshot) 。
这就是所谓的 闭包陷阱。
我们来分析一下刚刚的代码:
- 第一次点击:
messages = [msg1, msg2]
- 执行
setMessages([...messages, userMsg1])
→ 新状态是[msg1, msg2, userMsg1]
- 第二次点击时,此时函数作用域中的
messages
还是旧的[msg1, msg2]
- 执行
setMessages([...messages, userMsg2])
→新状态是[msg1, msg2, userMsg2]
-> 还是基于旧的 messages 添加新消息 ,userMsg1被覆盖!!
这就导致了状态更新的混乱,当渲染时,遇到
msg.role === 'user' ? styles.messageRight : styles.messageLeft
,会理所应当的只显示助手,而之前用户的消息记录没了。
正确做法:使用函数式更新
React 的 setState
支持传入一个函数,这个函数的参数是当前最新的状态值。这样可以避免闭包陷阱:
jsx
// ✅ 使用函数式更新,确保拿到最新的 messages
setMessages(prev => [...prev, { role: 'user', content: text }]);
const newMessage = await chat([{ role: 'user', content: text }]);
// ✅ 再次使用函数式更新
setMessages(prev => [...prev, newMessage.data]);
4. 集成聊天API
4.1 定义聊天函数
创建一个异步函数 chat
来封装与聊天API的交互过程。该函数接收消息对象作为参数,并返回助手的回复。
可以去deepseek 官方文档中查看各种模型和使用方法:api-docs.deepseek.com/zh-cn/
或者使用其他大模型如MoonShot :platform.moonshot.cn/docs/introd...
js
const DEEPSEEK_CHAT_API_URL = 'https://api.deepseek.com/chat/completions';
// 聊天Chat AI
export const chat = async (
messages, //一个数组,包含用户和助手之间的对话历史,每个元素代表一条消息
api_url = DEEPSEEK_CHAT_API_URL,//API请求的URL
api_key = import.meta.env.VITE_DEEPSEEK_API_KEY, // API访问所需的密钥,从环境变量`VITE_DEEPSEEK_API_KEY`中获取,.env.local中
model = 'deepseek-chat' // 使用的语言模型名称
) => {
try {
const response = await fetch(api_url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${api_key}`
},
body: JSON.stringify({
model,
messages,
stream: false,// 不开启流式输出
})
})
const data = await response.json();
return {
code: 0,
data: {
role: 'assistant',
content: data.choices[0].message.content
}
}
} catch(err) {
return {
code: 0,
msg: '出错了...'
}
}
}
- 发起HTTP请求 :使用fetch API向指定的
api_url
发起POST请求,发送的信息包括所选的模型、对话历史以及是否开启流式传输等信息。 - 处理响应 :一旦收到服务器的响应,就将其解析为JSON格式,并从中提取出助手的回答内容(假设API返回的数据结构中包含
choices[0].message.content
字段作为回答)。 - 错误处理:如果请求过程中出现任何异常,都会捕获该异常并返回一个带有错误码和简单错误信息的对象。
返回值
- 成功时:返回一个对象,其中包含状态码
code: 0
和助手的回答数据。 - 失败时:返回一个对象,包含状态码
code: 1
和错误信息msg: '出错了...'
。
结尾
通过本文,我们学习了如何利用 React
和 react-vant
快速搭建一个具备实际功能的旅游智能客服模块。从界面设计到数据处理再到与后端API的集成,每一步都展示了如何高效地构建用户友好的前端应用。
希望这篇文章能够帮助你更好地理解并实践类似的开发流程。未来,我们可以进一步探讨如何优化性能、增强安全性以及扩展更多高级功能。