Appearance
第76天:智能客服系统-前端开发
学习目标
- 掌握前端架构设计
- 学习对话界面开发
- 理解状态管理
- 掌握API集成
- 学习用户体验优化
前端架构设计
技术栈选择
typescript
interface TechStack {
framework: string;
uiLibrary: string;
stateManagement: string;
routing: string;
httpClient: string;
buildTool: string;
testing: string;
}
const techStack: TechStack = {
framework: "React 18",
uiLibrary: "Ant Design 5",
stateManagement: "Redux Toolkit",
routing: "React Router 6",
httpClient: "Axios",
buildTool: "Vite",
testing: "Jest + React Testing Library"
};项目结构
src/
├── components/
│ ├── Chat/
│ │ ├── ChatContainer.tsx
│ │ ├── MessageList.tsx
│ │ ├── MessageInput.tsx
│ │ └── TypingIndicator.tsx
│ ├── Ticket/
│ │ ├── TicketList.tsx
│ │ ├── TicketDetail.tsx
│ │ └── TicketForm.tsx
│ └── Shared/
│ ├── Layout.tsx
│ ├── Header.tsx
│ └── Sidebar.tsx
├── pages/
│ ├── ChatPage.tsx
│ ├── TicketPage.tsx
│ └── KnowledgePage.tsx
├── store/
│ ├── chatSlice.ts
│ ├── ticketSlice.ts
│ └── index.ts
├── services/
│ ├── api.ts
│ ├── chatService.ts
│ └── ticketService.ts
├── hooks/
│ ├── useChat.ts
│ └── useTicket.ts
├── types/
│ ├── chat.ts
│ └── ticket.ts
└── utils/
├── helpers.ts
└── constants.ts对话界面开发
聊天容器组件
typescript
import React, { useState, useEffect } from 'react';
import { Layout, Card } from 'antd';
import MessageList from './MessageList';
import MessageInput from './MessageInput';
import TypingIndicator from './TypingIndicator';
import { useChat } from '../../hooks/useChat';
import { Message } from '../../types/chat';
const { Content } = Layout;
interface ChatContainerProps {
sessionId?: string;
userId?: string;
}
const ChatContainer: React.FC<ChatContainerProps> = ({
sessionId,
userId
}) => {
const [isTyping, setIsTyping] = useState(false);
const {
messages,
sendMessage,
loading,
error
} = useChat(sessionId, userId);
const handleSendMessage = async (content: string) => {
setIsTyping(true);
try {
await sendMessage(content);
} catch (err) {
console.error('发送消息失败:', err);
} finally {
setIsTyping(false);
}
};
return (
<Layout style={{ height: '100vh' }}>
<Content style={{ padding: '24px' }}>
<Card
title="智能客服"
style={{ height: '100%' }}
bodyStyle={{
display: 'flex',
flexDirection: 'column',
height: 'calc(100% - 57px)'
}}
>
<div style={{ flex: 1, overflow: 'auto', marginBottom: '16px' }}>
<MessageList messages={messages} />
</div>
{isTyping && <TypingIndicator />}
<MessageInput
onSend={handleSendMessage}
disabled={loading || isTyping}
/>
</Card>
</Content>
</Layout>
);
};
export default ChatContainer;消息列表组件
typescript
import React from 'react';
import { List, Avatar, Tag, Space } from 'antd';
import { UserOutlined, RobotOutlined } from '@ant-design/icons';
import { Message } from '../../types/chat';
interface MessageListProps {
messages: Message[];
}
const MessageList: React.FC<MessageListProps> = ({ messages }) => {
const renderMessage = (message: Message) => {
const isUser = message.role === 'user';
return (
<div
key={message.id}
style={{
display: 'flex',
justifyContent: isUser ? 'flex-end' : 'flex-start',
marginBottom: '16px'
}}
>
<Space direction="vertical" size={4}>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '8px'
}}
>
{isUser ? (
<>
<span style={{ fontSize: '12px', color: '#999' }}>
{new Date(message.timestamp).toLocaleTimeString()}
</span>
<Avatar icon={<UserOutlined />} />
</>
) : (
<>
<Avatar icon={<RobotOutlined />} />
<span style={{ fontSize: '12px', color: '#999' }}>
{new Date(message.timestamp).toLocaleTimeString()}
</span>
</>
)}
</div>
<div
style={{
maxWidth: '70%',
padding: '12px 16px',
borderRadius: '8px',
backgroundColor: isUser ? '#1890ff' : '#f0f0f0',
color: isUser ? '#fff' : '#333',
wordBreak: 'break-word'
}}
>
{message.content}
</div>
{message.intent && (
<Tag color="blue" style={{ fontSize: '12px' }}>
意图: {message.intent}
</Tag>
)}
{message.sentiment && (
<Tag
color={message.sentiment === 'positive' ? 'green' :
message.sentiment === 'negative' ? 'red' : 'default'}
style={{ fontSize: '12px' }}
>
情感: {message.sentiment}
</Tag>
)}
</Space>
</div>
);
};
return (
<div>
{messages.length === 0 ? (
<div
style={{
textAlign: 'center',
color: '#999',
marginTop: '100px'
}}
>
<p>开始对话吧!</p>
</div>
) : (
messages.map(renderMessage)
)}
</div>
);
};
export default MessageList;消息输入组件
typescript
import React, { useState, useRef, useEffect } from 'react';
import { Input, Button, Space, message } from 'antd';
import { SendOutlined, PaperClipOutlined } from '@ant-design/icons';
const { TextArea } = Input;
interface MessageInputProps {
onSend: (content: string) => Promise<void>;
disabled?: boolean;
}
const MessageInput: React.FC<MessageInputProps> = ({
onSend,
disabled = false
}) => {
const [content, setContent] = useState('');
const [isSending, setIsSending] = useState(false);
const textAreaRef = useRef<any>(null);
useEffect(() => {
if (textAreaRef.current) {
textAreaRef.current.focus();
}
}, []);
const handleSend = async () => {
if (!content.trim()) {
message.warning('请输入消息内容');
return;
}
setIsSending(true);
try {
await onSend(content);
setContent('');
} catch (error) {
message.error('发送失败,请重试');
} finally {
setIsSending(false);
}
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
const handleFileUpload = () => {
message.info('文件上传功能开发中');
};
return (
<div style={{ display: 'flex', gap: '8px' }}>
<Button
icon={<PaperClipOutlined />}
onClick={handleFileUpload}
disabled={disabled || isSending}
/>
<TextArea
ref={textAreaRef}
value={content}
onChange={(e) => setContent(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="输入消息,按Enter发送,Shift+Enter换行"
autoSize={{ minRows: 1, maxRows: 4 }}
disabled={disabled || isSending}
style={{ flex: 1 }}
/>
<Button
type="primary"
icon={<SendOutlined />}
onClick={handleSend}
disabled={disabled || isSending || !content.trim()}
loading={isSending}
>
发送
</Button>
</div>
);
};
export default MessageInput;打字指示器
typescript
import React from 'react';
import { Space } from 'antd';
const TypingIndicator: React.FC = () => {
return (
<div style={{ padding: '12px 16px' }}>
<Space size={4}>
<span
style={{
width: '8px',
height: '8px',
borderRadius: '50%',
backgroundColor: '#1890ff',
animation: 'bounce 1.4s infinite ease-in-out both'
}}
/>
<span
style={{
width: '8px',
height: '8px',
borderRadius: '50%',
backgroundColor: '#1890ff',
animation: 'bounce 1.4s infinite ease-in-out both 0.16s'
}}
/>
<span
style={{
width: '8px',
height: '8px',
borderRadius: '50%',
backgroundColor: '#1890ff',
animation: 'bounce 1.4s infinite ease-in-out both 0.32s'
}}
/>
</Space>
<style>{`
@keyframes bounce {
0%, 80%, 100% {
transform: scale(0);
}
40% {
transform: scale(1);
}
}
`}</style>
</div>
);
};
export default TypingIndicator;状态管理
Redux Store配置
typescript
import { configureStore } from '@reduxjs/toolkit';
import chatReducer from './chatSlice';
import ticketReducer from './ticketSlice';
export const store = configureStore({
reducer: {
chat: chatReducer,
ticket: ticketReducer
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: ['chat/addMessage/fulfilled'],
ignoredPaths: ['chat.messages']
}
})
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;Chat Slice
typescript
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import { Message } from '../types/chat';
import { chatService } from '../services/chatService';
interface ChatState {
messages: Message[];
sessionId: string | null;
loading: boolean;
error: string | null;
isTyping: boolean;
}
const initialState: ChatState = {
messages: [],
sessionId: null,
loading: false,
error: null,
isTyping: false
};
export const sendMessage = createAsyncThunk(
'chat/sendMessage',
async ({ content, sessionId, userId }: {
content: string;
sessionId?: string;
userId?: string
}) => {
const response = await chatService.sendMessage({
message: content,
session_id: sessionId,
user_id: userId
});
return response;
}
);
export const getConversationHistory = createAsyncThunk(
'chat/getConversationHistory',
async (sessionId: string) => {
const response = await chatService.getConversationHistory(sessionId);
return response;
}
);
const chatSlice = createSlice({
name: 'chat',
initialState,
reducers: {
setSessionId: (state, action: PayloadAction<string>) => {
state.sessionId = action.payload;
},
clearMessages: (state) => {
state.messages = [];
state.sessionId = null;
},
setTyping: (state, action: PayloadAction<boolean>) => {
state.isTyping = action.payload;
}
},
extraReducers: (builder) => {
builder
.addCase(sendMessage.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(sendMessage.fulfilled, (state, action) => {
state.loading = false;
state.sessionId = action.payload.session_id;
const userMessage: Message = {
id: `msg_${Date.now()}_user`,
role: 'user',
content: action.meta.arg.content,
timestamp: new Date().toISOString()
};
const assistantMessage: Message = {
id: action.payload.message_id,
role: 'assistant',
content: action.payload.response,
timestamp: new Date().toISOString(),
intent: action.payload.intent,
sentiment: action.payload.sentiment
};
state.messages.push(userMessage, assistantMessage);
})
.addCase(sendMessage.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message || '发送消息失败';
})
.addCase(getConversationHistory.fulfilled, (state, action) => {
state.messages = action.payload;
});
}
});
export const { setSessionId, clearMessages, setTyping } = chatSlice.actions;
export default chatSlice.reducer;Ticket Slice
typescript
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import { Ticket } from '../types/ticket';
import { ticketService } from '../services/ticketService';
interface TicketState {
tickets: Ticket[];
currentTicket: Ticket | null;
loading: boolean;
error: string | null;
}
const initialState: TicketState = {
tickets: [],
currentTicket: null,
loading: false,
error: null
};
export const createTicket = createAsyncThunk(
'ticket/createTicket',
async (ticketData: {
user_id: string;
title: string;
description: string;
priority?: string;
conversation_id?: string;
}) => {
const response = await ticketService.createTicket(ticketData);
return response;
}
);
export const getTickets = createAsyncThunk(
'ticket/getTickets',
async ({ userId, status }: { userId?: string; status?: string }) => {
const response = await ticketService.getTickets(userId, status);
return response;
}
);
export const updateTicket = createAsyncThunk(
'ticket/updateTicket',
async ({ ticketId, updates }: { ticketId: string; updates: any }) => {
const response = await ticketService.updateTicket(ticketId, updates);
return response;
}
);
const ticketSlice = createSlice({
name: 'ticket',
initialState,
reducers: {
setCurrentTicket: (state, action: PayloadAction<Ticket | null>) => {
state.currentTicket = action.payload;
},
clearTickets: (state) => {
state.tickets = [];
state.currentTicket = null;
}
},
extraReducers: (builder) => {
builder
.addCase(createTicket.fulfilled, (state, action) => {
state.tickets.unshift(action.payload);
})
.addCase(getTickets.fulfilled, (state, action) => {
state.tickets = action.payload;
})
.addCase(updateTicket.fulfilled, (state, action) => {
const index = state.tickets.findIndex(
t => t.id === action.payload.id
);
if (index !== -1) {
state.tickets[index] = action.payload;
}
if (state.currentTicket?.id === action.payload.id) {
state.currentTicket = action.payload;
}
});
}
});
export const { setCurrentTicket, clearTickets } = ticketSlice.actions;
export default ticketSlice.reducer;API集成
API服务
typescript
import axios, { AxiosInstance, AxiosError } from 'axios';
class APIService {
private client: AxiosInstance;
constructor(baseURL: string) {
this.client = axios.create({
baseURL,
timeout: 30000,
headers: {
'Content-Type': 'application/json'
}
});
this.client.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
this.client.interceptors.response.use(
(response) => response,
(error: AxiosError) => {
if (error.response?.status === 401) {
localStorage.removeItem('token');
window.location.href = '/login';
}
return Promise.reject(error);
}
);
}
async get<T>(url: string, params?: any): Promise<T> {
const response = await this.client.get<T>(url, { params });
return response.data;
}
async post<T>(url: string, data?: any): Promise<T> {
const response = await this.client.post<T>(url, data);
return response.data;
}
async put<T>(url: string, data?: any): Promise<T> {
const response = await this.client.put<T>(url, data);
return response.data;
}
async delete<T>(url: string): Promise<T> {
const response = await this.client.delete<T>(url);
return response.data;
}
}
export const apiService = new APIService(
import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000/api'
);Chat服务
typescript
import { apiService } from './api';
import { Message } from '../types/chat';
interface SendMessageRequest {
message: string;
session_id?: string;
user_id?: string;
}
interface SendMessageResponse {
session_id: string;
message_id: string;
response: string;
intent?: string;
sentiment?: string;
confidence?: number;
}
export const chatService = {
async sendMessage(request: SendMessageRequest): Promise<SendMessageResponse> {
return apiService.post<SendMessageResponse>('/chat', request);
},
async getConversationHistory(sessionId: string): Promise<Message[]> {
return apiService.get<Message[]>(`/chat/history/${sessionId}`);
},
async clearConversation(sessionId: string): Promise<void> {
return apiService.delete<void>(`/chat/${sessionId}`);
}
};Ticket服务
typescript
import { apiService } from './api';
import { Ticket } from '../types/ticket';
interface CreateTicketRequest {
user_id: string;
title: string;
description: string;
priority?: string;
conversation_id?: string;
}
export const ticketService = {
async createTicket(request: CreateTicketRequest): Promise<Ticket> {
return apiService.post<Ticket>('/tickets', request);
},
async getTickets(userId?: string, status?: string): Promise<Ticket[]> {
const params: any = {};
if (userId) params.user_id = userId;
if (status) params.status = status;
return apiService.get<Ticket[]>('/tickets', params);
},
async getTicket(ticketId: string): Promise<Ticket> {
return apiService.get<Ticket>(`/tickets/${ticketId}`);
},
async updateTicket(ticketId: string, updates: any): Promise<Ticket> {
return apiService.put<Ticket>(`/tickets/${ticketId}`, updates);
},
async deleteTicket(ticketId: string): Promise<void> {
return apiService.delete<void>(`/tickets/${ticketId}`);
}
};实践练习
练习1:创建聊天组件
typescript
function createChatComponent() {
const ChatContainer = () => {
return (
<div>
<MessageList />
<MessageInput />
</div>
);
};
return ChatContainer;
}练习2:实现状态管理
typescript
function implementStateManagement() {
const store = configureStore({
reducer: {
chat: chatReducer,
ticket: ticketReducer
}
});
return store;
}练习3:集成API服务
typescript
function integrateAPIService() {
const apiService = new APIService('http://localhost:8000/api');
return apiService;
}总结
本节我们学习了智能客服系统前端开发:
- 前端架构设计
- 对话界面开发
- 状态管理
- API集成
前端是用户与系统交互的重要界面。
