bw-expo-app/components/templates/template-list.tsx

183 lines
4.4 KiB
TypeScript

import { useThemeColor } from '@/hooks/use-theme-color';
import { Template } from '@/lib/types/template';
import { ActivityIndicator, FlatList, RefreshControl, StyleSheet, View } from 'react-native';
import { ThemedText } from '../themed-text';
import { ThemedView } from '../themed-view';
import { TemplateCard } from './template-card';
interface TemplateListProps {
templates: Template[];
loading?: boolean;
refreshing?: boolean;
isLoadingMore?: boolean;
hasMore?: boolean;
onRefresh?: () => void;
onLoadMore?: () => void;
onTemplatePress?: (template: Template) => void;
onVideoChange?: (template: Template, index: number) => void;
error?: string | null;
}
export function TemplateList({
templates,
loading = false,
refreshing = false,
isLoadingMore = false,
hasMore = true,
onRefresh,
onLoadMore,
onTemplatePress,
onVideoChange,
error,
}: TemplateListProps) {
const tintColor = useThemeColor({}, 'tint');
const errorColor = useThemeColor({}, 'error');
// 渲染底部加载指示器
const renderFooter = () => {
if (isLoadingMore) {
return (
<View style={styles.footerLoader}>
<ActivityIndicator size="small" color={tintColor} />
<ThemedText style={styles.footerText}>...</ThemedText>
</View>
);
}
if (!hasMore && templates.length > 0) {
return (
<View style={styles.noMoreContainer}>
<ThemedText style={styles.noMoreText}> </ThemedText>
</View>
);
}
return null;
};
if (loading && templates.length === 0) {
return (
<ThemedView style={styles.centerContainer}>
<ActivityIndicator size="large" color={tintColor} />
<ThemedText style={styles.loadingText}>...</ThemedText>
</ThemedView>
);
}
if (error) {
return (
<ThemedView style={styles.centerContainer}>
<ThemedText style={[styles.errorText, { color: errorColor }]}>
{error}
</ThemedText>
</ThemedView>
);
}
if (templates.length === 0) {
return (
<ThemedView style={styles.centerContainer}>
<ThemedText style={styles.emptyText}></ThemedText>
{!hasMore && (
<View style={styles.noMoreContainer}>
<ThemedText style={styles.noMoreText}> </ThemedText>
</View>
)}
</ThemedView>
);
}
return (
<FlatList
data={templates}
keyExtractor={(item) => item.id}
renderItem={({ item, index }) => (
<TemplateCard
template={item}
onPress={onTemplatePress}
allTemplates={templates}
currentIndex={index}
onVideoChange={onVideoChange}
/>
)}
numColumns={2}
columnWrapperStyle={styles.row}
contentContainerStyle={styles.listContent}
refreshControl={
onRefresh ? (
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
colors={[tintColor]}
tintColor={tintColor}
progressViewOffset={70}
/>
) : undefined
}
onEndReached={onLoadMore}
onEndReachedThreshold={0.2}
ListFooterComponent={renderFooter}
ListFooterComponentStyle={templates.length > 0 ? styles.footerContainer : null}
showsVerticalScrollIndicator={false}
removeClippedSubviews={true}
maxToRenderPerBatch={10}
initialNumToRender={6}
windowSize={10}
/>
);
}
const styles = StyleSheet.create({
centerContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
listContent: {
paddingHorizontal: 14,
paddingTop: 70,
paddingBottom: 80,
},
row: {
justifyContent: 'space-between',
paddingHorizontal: 2,
},
loadingText: {
marginTop: 12,
fontSize: 16,
opacity: 0.7,
},
errorText: {
fontSize: 16,
textAlign: 'center',
},
emptyText: {
fontSize: 16,
opacity: 0.7,
},
footerLoader: {
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
paddingVertical: 20,
gap: 8,
},
footerText: {
fontSize: 14,
opacity: 0.7,
},
footerContainer: {
paddingBottom: 20,
},
noMoreContainer: {
paddingVertical: 20,
alignItems: 'center',
},
noMoreText: {
fontSize: 14,
opacity: 0.5,
color: '#666',
},
});