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

View File

@ -1,76 +1,55 @@
import { Message, Video } from '@/types/ask';
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 {
FlatList,
FlatListProps,
SafeAreaView
} from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import MessageItem from './aiChat';
interface ChatProps {
// 继承 FlatListProps 来接收所有 FlatList 的属性
interface ChatProps extends Omit<FlatListProps<Message>, 'data' | 'renderItem'> {
userMessages: Message[];
sessionId: string;
setSelectedImages: Dispatch<SetStateAction<string[]>>;
selectedImages: string[];
}
function ChatComponent({ userMessages, sessionId, setSelectedImages, selectedImages }: ChatProps) {
const flatListRef = useRef<FlatList>(null);
function ChatComponent(
{ userMessages, sessionId, setSelectedImages, selectedImages, ...restProps }: ChatProps,
ref: ForwardedRef<FlatList<Message>>
) {
const insets = useSafeAreaInsets();
const [modalVisible, setModalVisible] = React.useState({ visible: false, data: {} as Video | MaterialItem });
const { t } = useTranslation();
// 使用 useCallback 缓存 keyExtractor 函数
const keyExtractor = useCallback((item: Message) => `${item.role}-${item.timestamp}`, []);
// 使用 useMemo 缓存样式对象
const contentContainerStyle = useMemo(() => ({ padding: 16 }), []);
const contentContainerStyle = useMemo(() => ({ padding: 16, flexGrow: 1 }), []);
// 详情弹窗
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 (
<SafeAreaView className='flex-1'>
<SafeAreaView style={{ flex: 1 }}>
<FlatList
ref={flatListRef}
ref={ref}
data={userMessages}
keyExtractor={keyExtractor}
renderItem={({ item }) => MessageItem({ t, setSelectedImages, selectedImages, insets, item, sessionId, modalVisible, setModalVisible, setModalDetailsVisible, modalDetailsVisible })}
contentContainerStyle={contentContainerStyle}
keyboardDismissMode="interactive"
keyboardShouldPersistTaps="handled"
removeClippedSubviews={true}
maxToRenderPerBatch={10}
updateCellsBatchingPeriod={50}
initialNumToRender={10}
windowSize={11}
getItemLayout={getItemLayout}
renderItem={({ item }) => MessageItem({ t, setSelectedImages, selectedImages, insets, item, sessionId, modalVisible, setModalVisible, setModalDetailsVisible, modalDetailsVisible })}
{...restProps} // 将所有其他属性传递给 FlatList
/>
</SafeAreaView>
);
}
// 使用 React.memo 包装组件,避免不必要的重渲染
export default memo(ChatComponent);
export default memo(forwardRef(ChatComponent));