import { Asset } from 'expo-asset';
import { useFocusEffect, useRouter } from 'expo-router';
import * as SplashScreen from 'expo-splash-screen';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { FlatList, InteractionManager, PixelRatio, Platform, RefreshControl, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
// 懒加载组件
import ChatSvg from '@/assets/icons/svg/chat.svg';
import ErrorBoundary from '@/components/common/ErrorBoundary';
import UploaderProgress from '@/components/file-upload/upload-progress/uploader-progress';
import SkeletonItem from '@/components/memo/SkeletonItem';
// 类型定义
import { Fonts } from '@/constants/Fonts';
import { useUploadManager } from '@/hooks/useUploadManager';
import { getCachedData, prefetchChatDetail, prefetchChats } from '@/lib/prefetch';
import { fetchApi } from '@/lib/server-api-util';
import { Chat, getMessageText } from '@/types/ask';
import { useTranslation } from 'react-i18next';
// 预加载资源
const preloadAssets = async () => {
try {
await Asset.loadAsync([
require('@/assets/icons/svg/chat.svg'),
]);
} catch (error) {
// console.error('资源预加载失败:', error);
}
};
// 骨架屏占位
const SkeletonList = () => (
{Array(5).fill(0).map((_, index) => (
))}
);
const MemoList = () => {
const router = useRouter();
const insets = useSafeAreaInsets();
const [isMounted, setIsMounted] = useState(false);
const [historyList, setHistoryList] = useState([]);
const [isRefreshing, setIsRefreshing] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const flatListRef = useRef(null);
const { t } = useTranslation();
// 从缓存或API获取数据
const fetchHistoryList = useCallback(async (forceRefresh = false) => {
try {
setIsLoading(true);
// 先检查缓存
const cachedData = getCachedData('/chats');
if (cachedData && !forceRefresh) {
setHistoryList(cachedData);
}
// 总是从服务器获取最新数据
const data = await fetchApi('/chats');
setHistoryList(data);
// 预加载第一个聊天的详情
if (data.length > 0) {
InteractionManager.runAfterInteractions(() => {
prefetchChatDetail(data[0].session_id).catch(console.error);
});
}
return data;
} catch (error) {
console.error('获取历史记录失败:', error);
throw error;
} finally {
setIsLoading(false);
setIsRefreshing(false);
}
}, []);
// 处理下拉刷新
const handleRefresh = useCallback(() => {
if (isRefreshing) return;
setIsRefreshing(true);
fetchHistoryList(true).finally(() => {
setIsRefreshing(false);
});
}, [fetchHistoryList, isRefreshing]);
// 处理聊天项点击
const handleMemoPress = useCallback((item: Chat) => {
router.push({
pathname: '/ask',
params: { sessionId: item.session_id },
});
}, [router]);
// 初始加载和预加载
useEffect(() => {
let isActive = true;
const initialize = async () => {
try {
// 并行预加载资源和数据
await Promise.all([
preloadAssets(),
prefetchChats().then((data) => {
if (isActive && data) {
setHistoryList(data as Chat[]);
}
}),
]);
// 主数据加载
await fetchHistoryList();
} catch (error) {
console.error('初始化失败:', error);
} finally {
if (isActive) {
setIsMounted(true);
// 延迟隐藏启动画面
setTimeout(SplashScreen.hideAsync, 500);
}
}
};
initialize();
return () => {
isActive = false;
};
}, [fetchHistoryList]);
// 每次进入页面就刷新
useFocusEffect(
useCallback(() => {
handleRefresh();
}, [])
);
// 渲染列表项
const renderItem = useCallback(({ item }: { item: Chat }) => (
handleMemoPress(item)}
activeOpacity={0.7}
>
{item.title || t('ask:ask.unNamed')}
{(item.latest_message && getMessageText(item.latest_message)) || t('ask:ask.noMessage')}
), [handleMemoPress]);
// 渲染列表头部
const renderHeader = useCallback(() => (
{t('ask:ask.memoList')}
{/* 上传进度 */}
{/* */}
), [insets.top]);
// 上传进度组件
const UploadProgressSection = () => {
const { progressInfo, uploadSessionStartTime } = useUploadManager();
if (!uploadSessionStartTime || progressInfo.total <= 0) {
return null;
}
return (
);
};
// 空状态
const renderEmptyComponent = useCallback(() => (
{t('ask:ask.noChat')}
{isRefreshing ? t('ask:ask.loading') : t('ask:ask.refresh')}
), [handleRefresh, isRefreshing]);
// 如果组件未完全加载,显示骨架屏
if (!isMounted) {
return (
);
}
return (
item.session_id}
ListHeaderComponent={renderHeader}
ListEmptyComponent={!isLoading ? renderEmptyComponent : null}
refreshControl={
}
initialNumToRender={10}
maxToRenderPerBatch={5}
updateCellsBatchingPeriod={50}
windowSize={11} // 5 screens in each direction (5 + 1 + 5)
removeClippedSubviews={Platform.OS === 'android'}
getItemLayout={(data, index) => ({
length: 80,
offset: 80 * index,
index,
})}
contentContainerStyle={styles.listContent}
ItemSeparatorComponent={() => }
/>
);
};
// 使用React.memo优化组件
const MemoizedMemoList = React.memo(MemoList);
export default MemoizedMemoList;
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
headerContainer: {
paddingBottom: 8,
backgroundColor: '#fff',
},
title: {
fontSize: 20,
fontWeight: 'bold',
color: '#4C320C',
textAlign: 'center',
marginBottom: 8,
fontFamily: Fonts["quicksand"]
},
listContent: {
paddingBottom: Platform.select({
ios: 30,
android: 20,
}),
},
skeletonContainer: {
flex: 1,
backgroundColor: '#fff',
paddingTop: 20,
},
memoItem: {
flexDirection: 'row',
alignItems: 'center',
padding: 16,
backgroundColor: '#fff',
},
placeholderIcon: {
width: 48,
height: 48,
backgroundColor: '#f0f0f0',
borderRadius: 24,
},
memoContent: {
flex: 1,
marginLeft: 12,
justifyContent: 'center',
gap: 2
},
memoTitle: {
fontSize: 16,
fontWeight: 'bold',
color: '#4C320C',
marginBottom: 4,
fontFamily: Fonts['sfPro']
},
memoSubtitle: {
fontSize: 14,
color: '#AC7E35',
fontFamily: Fonts['inter']
},
separator: {
height: 1 / PixelRatio.get(),
backgroundColor: '#f0f0f0',
marginLeft: 60,
},
emptyContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 40,
},
emptyText: {
fontSize: 16,
color: '#999',
marginBottom: 20,
},
refreshButton: {
backgroundColor: '#FFB645',
paddingHorizontal: 24,
paddingVertical: 10,
borderRadius: 20,
},
refreshText: {
color: '#fff',
fontSize: 14,
fontWeight: '500',
},
});