feat: chat优化

This commit is contained in:
jinyaqiu 2025-07-28 11:51:10 +08:00
parent c1a382474f
commit 1584ed7fad
2 changed files with 89 additions and 144 deletions

View File

@ -9,10 +9,10 @@ import { router, useLocalSearchParams } from "expo-router";
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import { import {
Animated, Animated,
FlatList,
Keyboard, Keyboard,
KeyboardAvoidingView, KeyboardAvoidingView,
Platform, Platform,
ScrollView,
StyleSheet, StyleSheet,
TextInput, TextInput,
TouchableOpacity, TouchableOpacity,
@ -23,14 +23,11 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
export default function AskScreen() { export default function AskScreen() {
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
// 在组件内部添加 ref const chatListRef = useRef<FlatList>(null);
const scrollViewRef = useRef<ScrollView>(null);
const [isHello, setIsHello] = useState(true); const [isHello, setIsHello] = useState(true);
const [conversationId, setConversationId] = useState<string | null>(null); const [conversationId, setConversationId] = useState<string | null>(null);
const [userMessages, setUserMessages] = useState<Message[]>([]); const [userMessages, setUserMessages] = useState<Message[]>([]);
// 选择图片
const [selectedImages, setSelectedImages] = useState<string[]>([]); const [selectedImages, setSelectedImages] = useState<string[]>([]);
// 动画值
const fadeAnim = useRef(new Animated.Value(1)).current; const fadeAnim = useRef(new Animated.Value(1)).current;
const fadeAnimChat = useRef(new Animated.Value(0)).current; const fadeAnimChat = useRef(new Animated.Value(0)).current;
@ -39,24 +36,27 @@ export default function AskScreen() {
newSession: string; newSession: string;
}>(); }>();
// 处理滚动到底部 - 优化版本 // 创建一个可复用的滚动函数
const scrollToBottom = useCallback((animated = true) => { const scrollToEnd = useCallback((animated = true) => {
if (scrollViewRef.current && !isHello) { if (chatListRef.current) {
// 使用更长的延迟确保内容渲染完成 setTimeout(() => chatListRef.current?.scrollToEnd({ animated }), 100);
setTimeout(() => {
scrollViewRef.current?.scrollToEnd({ animated });
}, 150);
} }
}, [isHello]); }, []);
useEffect(() => {
if (!isHello && userMessages.length > 0) {
scrollToEnd();
}
}, [userMessages, isHello, scrollToEnd]);
// 监听键盘显示/隐藏事件 - 增强版本
useEffect(() => { useEffect(() => {
const keyboardDidShowListener = Keyboard.addListener( const keyboardDidShowListener = Keyboard.addListener(
'keyboardDidShow', 'keyboardDidShow',
(e) => { (e) => {
// 键盘显示时滚动到底部
setTimeout(() => { setTimeout(() => {
scrollToBottom(true); if (!isHello) {
scrollToEnd();
}
}, 100); }, 100);
} }
); );
@ -64,9 +64,10 @@ export default function AskScreen() {
const keyboardDidHideListener = Keyboard.addListener( const keyboardDidHideListener = Keyboard.addListener(
'keyboardDidHide', 'keyboardDidHide',
() => { () => {
// 键盘隐藏时也滚动到底部(可选)
setTimeout(() => { setTimeout(() => {
scrollToBottom(false); if (!isHello) {
scrollToEnd(false);
}
}, 100); }, 100);
} }
); );
@ -75,20 +76,8 @@ export default function AskScreen() {
keyboardDidShowListener.remove(); keyboardDidShowListener.remove();
keyboardDidHideListener.remove(); keyboardDidHideListener.remove();
}; };
}, [scrollToBottom]); }, [isHello]);
// 处理消息变化时滚动 - 优化版本
useEffect(() => {
if (!isHello && userMessages.length > 0) {
// 消息变化时立即滚动到底部
const timer = setTimeout(() => {
scrollToBottom(true);
}, 50);
return () => clearTimeout(timer);
}
}, [userMessages.length, isHello, scrollToBottom]);
// 处理路由参数 - 优化版本
useEffect(() => { useEffect(() => {
if (sessionId) { if (sessionId) {
setConversationId(sessionId); setConversationId(sessionId);
@ -103,10 +92,8 @@ export default function AskScreen() {
} }
}, [sessionId, newSession]); }, [sessionId, newSession]);
// 动画效果
useEffect(() => { useEffect(() => {
if (isHello) { if (isHello) {
// 显示欢迎页,隐藏聊天页
Animated.parallel([ Animated.parallel([
Animated.timing(fadeAnim, { Animated.timing(fadeAnim, {
toValue: 1, toValue: 1,
@ -120,7 +107,6 @@ export default function AskScreen() {
}) })
]).start(); ]).start();
} else { } else {
// 显示聊天页,隐藏欢迎页
Animated.parallel([ Animated.parallel([
Animated.timing(fadeAnim, { Animated.timing(fadeAnim, {
toValue: 0, toValue: 0,
@ -133,31 +119,27 @@ export default function AskScreen() {
useNativeDriver: true, useNativeDriver: true,
}) })
]).start(() => { ]).start(() => {
// 动画完成后滚动到底部
setTimeout(() => { setTimeout(() => {
scrollToBottom(false); if (!isHello) {
scrollToEnd(false);
}
}, 50); }, 50);
}); });
} }
}, [isHello, fadeAnim, fadeAnimChat, scrollToBottom]); }, [isHello, fadeAnim, fadeAnimChat]);
// 页面进入时的处理 - 新增
useEffect(() => { useEffect(() => {
if (!isHello) { if (!isHello) {
// 页面完全加载后滚动到底部
const timer = setTimeout(() => { const timer = setTimeout(() => {
scrollToBottom(false); scrollToEnd(false);
}, 300); }, 300);
return () => clearTimeout(timer); return () => clearTimeout(timer);
} }
}, [isHello, scrollToBottom]); }, [isHello]);
// 新增:页面获得焦点时处理
useEffect(() => { useEffect(() => {
// 页面进入时确保键盘关闭并滚动到底部
const timer = setTimeout(() => { const timer = setTimeout(() => {
if (!isHello) { if (!isHello) {
// 让所有输入框失去焦点
try { try {
if (TextInput.State?.currentlyFocusedInput) { if (TextInput.State?.currentlyFocusedInput) {
const input = TextInput.State.currentlyFocusedInput(); const input = TextInput.State.currentlyFocusedInput();
@ -167,13 +149,12 @@ export default function AskScreen() {
console.log('失去焦点失败:', error); console.log('失去焦点失败:', error);
} }
// 滚动到底部 scrollToEnd(false);
scrollToBottom(false);
} }
}, 200); }, 200);
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, [isHello, scrollToBottom]); }, [isHello]);
return ( return (
<View style={[styles.container, { paddingTop: insets.top }]}> <View style={[styles.container, { paddingTop: insets.top }]}>
@ -182,7 +163,6 @@ export default function AskScreen() {
<TouchableOpacity <TouchableOpacity
style={styles.backButton} style={styles.backButton}
onPress={() => { onPress={() => {
// 确保关闭键盘
try { try {
if (TextInput.State?.currentlyFocusedInput) { if (TextInput.State?.currentlyFocusedInput) {
const input = TextInput.State.currentlyFocusedInput(); const input = TextInput.State.currentlyFocusedInput();
@ -201,64 +181,56 @@ export default function AskScreen() {
<View style={styles.placeholder} /> <View style={styles.placeholder} />
</View> </View>
<View style={styles.contentContainer}>
{/* 欢迎页面 */}
<Animated.View
style={[
styles.absoluteView,
{
opacity: fadeAnim,
pointerEvents: isHello ? 'auto' : 'none',
zIndex: 1
}
]}
>
<AskHello />
</Animated.View>
{/* 聊天页面 */}
<Animated.View
style={[
styles.absoluteView,
{
opacity: fadeAnimChat,
pointerEvents: isHello ? 'none' : 'auto',
zIndex: 0
}
]}
>
<Chat
ref={chatListRef}
userMessages={userMessages}
sessionId={sessionId}
setSelectedImages={setSelectedImages}
selectedImages={selectedImages}
style={styles.chatContainer}
contentContainerStyle={styles.chatContentContainer}
showsVerticalScrollIndicator={false}
onContentSizeChange={() => scrollToEnd()}
/>
</Animated.View>
</View>
{/* 输入框区域 */}
<KeyboardAvoidingView <KeyboardAvoidingView
style={styles.keyboardAvoidingView}
behavior={Platform.OS === "ios" ? "padding" : "height"} behavior={Platform.OS === "ios" ? "padding" : "height"}
enabled={!isHello} keyboardVerticalOffset={0} >
>
<View style={styles.contentContainer}>
{/* 欢迎页面 */}
<Animated.View
style={[
styles.absoluteView,
{
opacity: fadeAnim,
// 使用 pointerEvents 控制交互
pointerEvents: isHello ? 'auto' : 'none',
zIndex: 1
}
]}
>
<AskHello />
</Animated.View>
{/* 聊天页面 */}
<Animated.View
style={[
styles.absoluteView,
{
opacity: fadeAnimChat,
pointerEvents: isHello ? 'none' : 'auto',
zIndex: 0
}
]}
>
<ScrollView
ref={scrollViewRef}
style={styles.chatContainer}
contentContainerStyle={styles.chatContentContainer}
keyboardDismissMode="interactive"
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
onContentSizeChange={() => scrollToBottom(true)}
>
<Chat
userMessages={userMessages}
sessionId={sessionId}
setSelectedImages={setSelectedImages}
selectedImages={selectedImages}
/>
</ScrollView>
</Animated.View>
</View>
{/* 输入框 */}
<View style={styles.inputContainer} key={conversationId}> <View style={styles.inputContainer} key={conversationId}>
<SendMessage <SendMessage
setIsHello={setIsHello} setIsHello={setIsHello}
setUserMessages={setUserMessages}
setConversationId={setConversationId}
conversationId={conversationId} conversationId={conversationId}
setConversationId={setConversationId}
setUserMessages={setUserMessages}
selectedImages={selectedImages} selectedImages={selectedImages}
setSelectedImages={setSelectedImages} setSelectedImages={setSelectedImages}
/> />
@ -271,7 +243,7 @@ export default function AskScreen() {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
backgroundColor: 'white', backgroundColor: '#f8f8f8',
}, },
navbar: { navbar: {
flexDirection: 'row', flexDirection: 'row',
@ -280,10 +252,8 @@ const styles = StyleSheet.create({
paddingVertical: 16, paddingVertical: 16,
paddingHorizontal: 16, paddingHorizontal: 16,
backgroundColor: 'white', backgroundColor: 'white',
// 使用 border 替代阴影
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: 'rgba(0,0,0,0.1)', borderBottomColor: 'rgba(0,0,0,0.1)',
// 如果需要更柔和的边缘,可以添加一个微妙的阴影
elevation: 1, // Android elevation: 1, // Android
shadowColor: '#000', shadowColor: '#000',
shadowOffset: { width: 0, height: 1 }, shadowOffset: { width: 0, height: 1 },
@ -307,13 +277,9 @@ const styles = StyleSheet.create({
placeholder: { placeholder: {
width: 40, width: 40,
}, },
// 更新 keyboardAvoidingView 和 contentContainer 样式
keyboardAvoidingView: {
flex: 1,
},
contentContainer: { contentContainer: {
flex: 1, flex: 1,
justifyContent: 'center', position: 'relative',
}, },
absoluteView: { absoluteView: {
position: 'absolute', position: 'absolute',

View File

@ -1,76 +1,55 @@
import { Message, Video } from '@/types/ask'; import { Message, Video } from '@/types/ask';
import { MaterialItem } from '@/types/personal-info'; import { MaterialItem } from '@/types/personal-info';
import React, { Dispatch, memo, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import React, { Dispatch, ForwardedRef, forwardRef, memo, SetStateAction, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
FlatList, FlatList,
FlatListProps,
SafeAreaView SafeAreaView
} from 'react-native'; } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import MessageItem from './aiChat'; import MessageItem from './aiChat';
interface ChatProps { // 继承 FlatListProps 来接收所有 FlatList 的属性
interface ChatProps extends Omit<FlatListProps<Message>, 'data' | 'renderItem'> {
userMessages: Message[]; userMessages: Message[];
sessionId: string; sessionId: string;
setSelectedImages: Dispatch<SetStateAction<string[]>>; setSelectedImages: Dispatch<SetStateAction<string[]>>;
selectedImages: string[]; selectedImages: string[];
} }
function ChatComponent({ userMessages, sessionId, setSelectedImages, selectedImages }: ChatProps) { function ChatComponent(
const flatListRef = useRef<FlatList>(null); { userMessages, sessionId, setSelectedImages, selectedImages, ...restProps }: ChatProps,
ref: ForwardedRef<FlatList<Message>>
) {
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const [modalVisible, setModalVisible] = React.useState({ visible: false, data: {} as Video | MaterialItem }); const [modalVisible, setModalVisible] = React.useState({ visible: false, data: {} as Video | MaterialItem });
const { t } = useTranslation(); const { t } = useTranslation();
// 使用 useCallback 缓存 keyExtractor 函数
const keyExtractor = useCallback((item: Message) => `${item.role}-${item.timestamp}`, []); const keyExtractor = useCallback((item: Message) => `${item.role}-${item.timestamp}`, []);
// 使用 useMemo 缓存样式对象 const contentContainerStyle = useMemo(() => ({ padding: 16, flexGrow: 1 }), []);
const contentContainerStyle = useMemo(() => ({ padding: 16 }), []);
// 详情弹窗
const [modalDetailsVisible, setModalDetailsVisible] = useState<boolean>(false); const [modalDetailsVisible, setModalDetailsVisible] = useState<boolean>(false);
// 自动滚动到底部
useEffect(() => {
if (userMessages.length > 0) {
// 延迟滚动以确保渲染完成
const timer = setTimeout(() => {
flatListRef.current?.scrollToEnd({ animated: true });
}, 150);
return () => clearTimeout(timer);
}
}, [userMessages]);
// 优化 FlatList 性能 - 提供 getItemLayout 方法
const getItemLayout = useCallback((data: Message[] | null | undefined, index: number) => {
// 假设每个消息项的高度大约为 100可根据实际情况调整
const averageItemHeight = 100;
return {
length: averageItemHeight,
offset: averageItemHeight * index,
index,
};
}, []);
return ( return (
<SafeAreaView className='flex-1'> <SafeAreaView style={{ flex: 1 }}>
<FlatList <FlatList
ref={flatListRef} ref={ref}
data={userMessages} data={userMessages}
keyExtractor={keyExtractor} keyExtractor={keyExtractor}
renderItem={({ item }) => MessageItem({ t, setSelectedImages, selectedImages, insets, item, sessionId, modalVisible, setModalVisible, setModalDetailsVisible, modalDetailsVisible })}
contentContainerStyle={contentContainerStyle} contentContainerStyle={contentContainerStyle}
keyboardDismissMode="interactive" keyboardDismissMode="interactive"
keyboardShouldPersistTaps="handled"
removeClippedSubviews={true} removeClippedSubviews={true}
maxToRenderPerBatch={10} maxToRenderPerBatch={10}
updateCellsBatchingPeriod={50} updateCellsBatchingPeriod={50}
initialNumToRender={10} initialNumToRender={10}
windowSize={11} windowSize={11}
getItemLayout={getItemLayout} {...restProps} // 将所有其他属性传递给 FlatList
renderItem={({ item }) => MessageItem({ t, setSelectedImages, selectedImages, insets, item, sessionId, modalVisible, setModalVisible, setModalDetailsVisible, modalDetailsVisible })}
/> />
</SafeAreaView> </SafeAreaView>
); );
} }
// 使用 React.memo 包装组件,避免不必要的重渲染 export default memo(forwardRef(ChatComponent));
export default memo(ChatComponent);