You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
12 KiB
12 KiB
06-AI对话页面(原型)
目标
实现 APP 端 AI 健康问诊对话功能,使用模拟数据模拟多轮对话效果。
UI 设计参考
参考设计稿:
files/ui/问答页.png、files/ui/问答对话.png
前置要求
- 导航配置完成
- 模拟数据服务已创建(
src/mock/chat.ts)
实施步骤
步骤 1:创建对话状态 Store
创建 src/stores/useChatStore.ts:
import { create } from 'zustand'
import AsyncStorage from '@react-native-async-storage/async-storage'
import { Conversation, Message } from '../types'
interface ChatState {
conversations: Conversation[]
addConversation: (conv: Conversation) => void
deleteConversation: (id: string) => void
addMessage: (convId: string, message: Message) => void
}
export const useChatStore = create<ChatState>((set, get) => ({
conversations: [],
addConversation: (conv) => {
const updated = [conv, ...get().conversations]
AsyncStorage.setItem('conversations', JSON.stringify(updated))
set({ conversations: updated })
},
deleteConversation: (id) => {
const updated = get().conversations.filter((c) => c.id !== id)
AsyncStorage.setItem('conversations', JSON.stringify(updated))
set({ conversations: updated })
},
addMessage: (convId, message) => {
const updated = get().conversations.map((c) => {
if (c.id === convId) {
return {
...c,
messages: [...c.messages, message],
updatedAt: new Date().toISOString(),
}
}
return c
})
AsyncStorage.setItem('conversations', JSON.stringify(updated))
set({ conversations: updated })
},
}))
步骤 2:对话列表页面
创建 src/screens/chat/ChatListScreen.tsx:
import React from 'react'
import { View, FlatList, StyleSheet, TouchableOpacity, Alert } from 'react-native'
import { Text, FAB, Card, IconButton } from 'react-native-paper'
import Icon from 'react-native-vector-icons/MaterialCommunityIcons'
import { useNavigation } from '@react-navigation/native'
import dayjs from 'dayjs'
import { useChatStore } from '../../stores/useChatStore'
import type { ChatNavigationProp } from '../../navigation/types'
const ChatListScreen = () => {
const navigation = useNavigation<ChatNavigationProp>()
const { conversations, addConversation, deleteConversation } = useChatStore()
const handleCreate = () => {
const newConv = {
id: Date.now().toString(),
title: '新对话',
messages: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}
addConversation(newConv)
navigation.navigate('ChatDetail', { id: newConv.id })
}
const handleDelete = (id: string) => {
Alert.alert('确认删除', '确定要删除这个对话吗?', [
{ text: '取消', style: 'cancel' },
{ text: '删除', style: 'destructive', onPress: () => deleteConversation(id) },
])
}
const renderItem = ({ item }: { item: typeof conversations[0] }) => (
<TouchableOpacity onPress={() => navigation.navigate('ChatDetail', { id: item.id })}>
<Card style={styles.card}>
<Card.Content style={styles.cardContent}>
<View style={styles.cardIcon}>
<Icon name="chat-processing" size={24} color="#10B981" />
</View>
<View style={styles.cardInfo}>
<Text style={styles.cardTitle}>{item.title}</Text>
<Text style={styles.cardTime}>
{dayjs(item.updatedAt).format('MM-DD HH:mm')}
</Text>
</View>
<IconButton
icon="delete-outline"
size={20}
onPress={() => handleDelete(item.id)}
/>
</Card.Content>
</Card>
</TouchableOpacity>
)
return (
<View style={styles.container}>
{conversations.length === 0 ? (
<View style={styles.emptyContainer}>
<Icon name="chat-outline" size={64} color="#D1D5DB" />
<Text style={styles.emptyText}>暂无对话记录</Text>
<Text style={styles.emptySubtext}>点击下方按钮开始咨询</Text>
</View>
) : (
<FlatList
data={conversations}
renderItem={renderItem}
keyExtractor={(item) => item.id}
contentContainerStyle={styles.list}
/>
)}
<FAB
icon="plus"
style={styles.fab}
onPress={handleCreate}
label="新建对话"
color="#fff"
/>
</View>
)
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#F3F4F6' },
list: { padding: 16 },
card: { marginBottom: 12, borderRadius: 12 },
cardContent: { flexDirection: 'row', alignItems: 'center' },
cardIcon: { width: 48, height: 48, borderRadius: 24, backgroundColor: '#ECFDF5', justifyContent: 'center', alignItems: 'center' },
cardInfo: { flex: 1, marginLeft: 12 },
cardTitle: { fontSize: 16, fontWeight: '500', color: '#1F2937' },
cardTime: { fontSize: 12, color: '#9CA3AF', marginTop: 4 },
emptyContainer: { flex: 1, justifyContent: 'center', alignItems: 'center' },
emptyText: { fontSize: 16, color: '#6B7280', marginTop: 16 },
emptySubtext: { fontSize: 14, color: '#9CA3AF', marginTop: 4 },
fab: { position: 'absolute', right: 16, bottom: 16, backgroundColor: '#10B981' },
})
export default ChatListScreen
步骤 3:对话详情页面
创建 src/screens/chat/ChatDetailScreen.tsx:
import React, { useState, useRef } from 'react'
import {
View,
FlatList,
StyleSheet,
KeyboardAvoidingView,
Platform,
} from 'react-native'
import { Text, TextInput, IconButton, Avatar } from 'react-native-paper'
import { useRoute } from '@react-navigation/native'
import { useChatStore } from '../../stores/useChatStore'
import { useAuthStore } from '../../stores/useAuthStore'
import { mockAIReply } from '../../mock/chat'
import type { ChatDetailRouteProp } from '../../navigation/types'
import type { Message } from '../../types'
const ChatDetailScreen = () => {
const route = useRoute<ChatDetailRouteProp>()
const { id } = route.params
const { conversations, addMessage } = useChatStore()
const { user } = useAuthStore()
const flatListRef = useRef<FlatList>(null)
const conversation = conversations.find((c) => c.id === id)
const messages = conversation?.messages || []
const [inputText, setInputText] = useState('')
const [sending, setSending] = useState(false)
const handleSend = async () => {
const content = inputText.trim()
if (!content || sending) return
// 添加用户消息
const userMessage: Message = {
id: Date.now().toString(),
role: 'user',
content,
createdAt: new Date().toISOString(),
}
addMessage(id, userMessage)
setInputText('')
// 模拟AI回复
setSending(true)
try {
const reply = await mockAIReply(content)
const assistantMessage: Message = {
id: (Date.now() + 1).toString(),
role: 'assistant',
content: reply,
createdAt: new Date().toISOString(),
}
addMessage(id, assistantMessage)
} finally {
setSending(false)
}
}
const renderMessage = ({ item }: { item: Message }) => {
const isUser = item.role === 'user'
return (
<View style={[styles.messageRow, isUser && styles.messageRowUser]}>
{!isUser && (
<Avatar.Icon size={36} icon="robot" style={styles.avatarAI} />
)}
<View style={[styles.messageBubble, isUser ? styles.userBubble : styles.assistantBubble]}>
<Text style={isUser ? styles.userText : styles.assistantText}>
{item.content}
</Text>
</View>
{isUser && (
<Avatar.Text
size={36}
label={user?.nickname?.charAt(0) || 'U'}
style={styles.avatarUser}
/>
)}
</View>
)
}
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
keyboardVerticalOffset={90}
>
{/* 欢迎消息 */}
{messages.length === 0 && (
<View style={styles.welcomeContainer}>
<Avatar.Icon size={64} icon="robot" style={styles.welcomeAvatar} />
<Text style={styles.welcomeTitle}>AI健康助手</Text>
<Text style={styles.welcomeText}>
您好!我是AI健康助手,可以为您提供健康咨询和建议。
{'\n'}请描述您的症状或健康问题。
</Text>
</View>
)}
{/* 消息列表 */}
<FlatList
ref={flatListRef}
data={messages}
renderItem={renderMessage}
keyExtractor={(item) => item.id}
contentContainerStyle={styles.messageList}
onContentSizeChange={() => flatListRef.current?.scrollToEnd()}
/>
{/* 输入中提示 */}
{sending && (
<View style={styles.typingIndicator}>
<Text style={styles.typingText}>AI 正在思考...</Text>
</View>
)}
{/* 输入区域 */}
<View style={styles.inputContainer}>
<TextInput
style={styles.input}
value={inputText}
onChangeText={setInputText}
placeholder="请描述您的健康问题..."
multiline
maxLength={500}
disabled={sending}
/>
<IconButton
icon="send"
size={24}
iconColor="#fff"
style={styles.sendButton}
disabled={!inputText.trim() || sending}
onPress={handleSend}
/>
</View>
{/* 免责声明 */}
<View style={styles.disclaimer}>
<Text style={styles.disclaimerText}>
AI 建议仅供参考,不构成医疗诊断,如有需要请就医
</Text>
</View>
</KeyboardAvoidingView>
)
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#F3F4F6' },
welcomeContainer: { alignItems: 'center', padding: 32 },
welcomeAvatar: { backgroundColor: '#3B82F6' },
welcomeTitle: { fontSize: 20, fontWeight: '600', marginTop: 16, color: '#1F2937' },
welcomeText: { fontSize: 14, color: '#6B7280', textAlign: 'center', marginTop: 8, lineHeight: 22 },
messageList: { padding: 16 },
messageRow: { flexDirection: 'row', marginBottom: 16, alignItems: 'flex-end' },
messageRowUser: { flexDirection: 'row-reverse' },
avatarAI: { backgroundColor: '#3B82F6', marginRight: 8 },
avatarUser: { backgroundColor: '#10B981', marginLeft: 8 },
messageBubble: { maxWidth: '70%', padding: 12, borderRadius: 16 },
userBubble: { backgroundColor: '#10B981', borderBottomRightRadius: 4 },
assistantBubble: { backgroundColor: '#fff', borderBottomLeftRadius: 4 },
userText: { color: '#fff', fontSize: 14, lineHeight: 20 },
assistantText: { color: '#1F2937', fontSize: 14, lineHeight: 20 },
typingIndicator: { paddingHorizontal: 16, paddingVertical: 8 },
typingText: { color: '#9CA3AF', fontSize: 13 },
inputContainer: { flexDirection: 'row', alignItems: 'center', padding: 8, backgroundColor: '#fff', borderTopWidth: 1, borderTopColor: '#E5E7EB' },
input: { flex: 1, maxHeight: 100, backgroundColor: '#F3F4F6', borderRadius: 20, paddingHorizontal: 16, paddingVertical: 8 },
sendButton: { backgroundColor: '#10B981', marginLeft: 8 },
disclaimer: { padding: 8, backgroundColor: '#FEF3C7', alignItems: 'center' },
disclaimerText: { fontSize: 11, color: '#92400E' },
})
export default ChatDetailScreen
模拟数据说明
使用 src/mock/chat.ts 中的 mockAIReply 函数:
- 根据关键词匹配预设回答
- 支持:疲劳、失眠、关节痛等常见问题
- 回答格式包含:情况分析、建议、用药参考、产品推荐
验收标准
- 对话列表正常显示
- 新建对话正常
- 删除对话正常
- 消息发送和模拟回复正常
- 消息气泡样式正确
- 免责声明显示
预计耗时
35-45 分钟
下一步
完成后进入 02-APP原型开发/07-个人中心页面.md