fix: 修复布局问题
This commit is contained in:
@@ -1,14 +1,41 @@
|
||||
import { ThemedView } from '@/components/themed-view';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ActivityIndicator, Image, ScrollView, StyleSheet, Text, View } from 'react-native';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Image,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
RefreshControl,
|
||||
Platform,
|
||||
TouchableOpacity
|
||||
} from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { getTemplateGenerations, TemplateGeneration } from '@/lib/api/template-generations';
|
||||
import { router } from 'expo-router';
|
||||
|
||||
interface GroupedData {
|
||||
[date: string]: TemplateGeneration[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据文件 URL 后缀名判断媒体类型
|
||||
*/
|
||||
function getMediaType(url: string): 'image' | 'video' | 'unknown' {
|
||||
if (!url) return 'unknown';
|
||||
|
||||
const extension = url.split('.').pop()?.toLowerCase() || '';
|
||||
|
||||
const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg'];
|
||||
const videoExts = ['mp4', 'webm', 'avi', 'mov', 'mkv', 'flv', 'm4v'];
|
||||
|
||||
if (imageExts.includes(extension)) return 'image';
|
||||
if (videoExts.includes(extension)) return 'video';
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
const groupByDate = (data: TemplateGeneration[]): GroupedData => {
|
||||
const groups: GroupedData = {};
|
||||
|
||||
@@ -115,30 +142,127 @@ const getStatusText = (status: string): string => {
|
||||
export default function HistoryScreen() {
|
||||
const [groupedData, setGroupedData] = useState<GroupedData>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [page, setPage] = useState(1);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [allGenerations, setAllGenerations] = useState<TemplateGeneration[]>([]);
|
||||
|
||||
const fetchData = async (pageNum: number = 1, isRefresh: boolean = false) => {
|
||||
try {
|
||||
if (isRefresh) {
|
||||
setRefreshing(true);
|
||||
} else if (pageNum === 1) {
|
||||
setLoading(true);
|
||||
} else {
|
||||
setLoadingMore(true);
|
||||
}
|
||||
|
||||
const response = await getTemplateGenerations({ page: pageNum, limit: 20 });
|
||||
|
||||
if (response.success && response.data) {
|
||||
const newGenerations = response.data.generations;
|
||||
|
||||
if (isRefresh || pageNum === 1) {
|
||||
setAllGenerations(newGenerations);
|
||||
setPage(1);
|
||||
} else {
|
||||
setAllGenerations(prev => [...prev, ...newGenerations]);
|
||||
}
|
||||
|
||||
setHasMore(newGenerations.length === 20);
|
||||
const grouped = groupByDate(isRefresh || pageNum === 1 ? newGenerations : [...allGenerations, ...newGenerations]);
|
||||
setGroupedData(grouped);
|
||||
setError(null);
|
||||
} else {
|
||||
setError('获取数据失败');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch data:', err);
|
||||
setError(err instanceof Error ? err.message : '获取数据失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
setLoadingMore(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await getTemplateGenerations({ limit: 100 });
|
||||
|
||||
if (response.success && response.data) {
|
||||
const grouped = groupByDate(response.data.generations);
|
||||
setGroupedData(grouped);
|
||||
} else {
|
||||
setError('获取数据失败');
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '获取数据失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
fetchData(1);
|
||||
}, []);
|
||||
|
||||
const onRefresh = useCallback(() => {
|
||||
fetchData(1, true);
|
||||
}, []);
|
||||
|
||||
const loadMore = useCallback(() => {
|
||||
if (!loadingMore && hasMore && !loading) {
|
||||
const nextPage = page + 1;
|
||||
setPage(nextPage);
|
||||
fetchData(nextPage, false);
|
||||
}
|
||||
}, [loadingMore, hasMore, page, loading]);
|
||||
|
||||
const handleScroll = (event: any) => {
|
||||
const { layoutMeasurement, contentOffset, contentSize } = event.nativeEvent;
|
||||
const paddingToBottom = 20;
|
||||
const isCloseToBottom = layoutMeasurement.height + contentOffset.y >= contentSize.height - paddingToBottom;
|
||||
|
||||
if (isCloseToBottom && !loadingMore && hasMore) {
|
||||
loadMore();
|
||||
}
|
||||
};
|
||||
|
||||
const renderMediaItem = (item: TemplateGeneration) => {
|
||||
const url = item.resultUrl && item.resultUrl.length > 0 ? item.resultUrl[0] : null;
|
||||
|
||||
if (!url) {
|
||||
return (
|
||||
<View style={[styles.image, { height: getImageHeight(item.type) }, styles.placeholderImage]}>
|
||||
<Text style={styles.placeholderText}>
|
||||
{item.type === 'VIDEO' ? '🎬' : item.type === 'IMAGE' ? '🖼️' : '📝'}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const mediaType = getMediaType(url);
|
||||
|
||||
if (mediaType === 'video') {
|
||||
if (Platform.OS === 'web') {
|
||||
return (
|
||||
<video
|
||||
src={url}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: getImageHeight(item.type),
|
||||
objectFit: 'cover' as any,
|
||||
}}
|
||||
muted
|
||||
playsInline
|
||||
preload="metadata"
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<View style={[styles.image, { height: getImageHeight(item.type) }, styles.videoPlaceholder]}>
|
||||
<Text style={styles.videoIcon}>🎬</Text>
|
||||
<Text style={styles.videoText}>视频</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Image
|
||||
source={{ uri: url }}
|
||||
style={[styles.image, { height: getImageHeight(item.type) }]}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<ThemedView style={styles.canvas} lightColor="#050505" darkColor="#050505">
|
||||
@@ -173,6 +297,16 @@ export default function HistoryScreen() {
|
||||
<ScrollView
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={onRefresh}
|
||||
tintColor="#FFFFFF"
|
||||
colors={['#FFFFFF']}
|
||||
/>
|
||||
}
|
||||
onScroll={handleScroll}
|
||||
scrollEventThrottle={400}
|
||||
>
|
||||
<Text style={styles.heading}>Content Generation</Text>
|
||||
|
||||
@@ -190,19 +324,13 @@ export default function HistoryScreen() {
|
||||
<View style={styles.gallery}>
|
||||
<View style={styles.leadingLane}>
|
||||
{leftColumn.map(item => (
|
||||
<View key={item.id} style={styles.frame}>
|
||||
{item.resultUrl && item.resultUrl.length > 0 ? (
|
||||
<Image
|
||||
source={{ uri: item.resultUrl[0] }}
|
||||
style={[styles.image, { height: getImageHeight(item.type) }]}
|
||||
/>
|
||||
) : (
|
||||
<View style={[styles.image, { height: getImageHeight(item.type) }, styles.placeholderImage]}>
|
||||
<Text style={styles.placeholderText}>
|
||||
{item.type === 'VIDEO' ? '🎬' : item.type === 'IMAGE' ? '🖼️' : '📝'}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
key={item.id}
|
||||
style={styles.frame}
|
||||
onPress={() => router.push(`/result?generationId=${item.id}`)}
|
||||
activeOpacity={0.9}
|
||||
>
|
||||
{renderMediaItem(item)}
|
||||
|
||||
<View style={styles.overlay}>
|
||||
<View style={styles.statusBadge}>
|
||||
@@ -211,25 +339,19 @@ export default function HistoryScreen() {
|
||||
</View>
|
||||
<Text style={styles.templateName}>{item.template?.title || '未知模板'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<View style={styles.trailingLane}>
|
||||
{rightColumn.map(item => (
|
||||
<View key={item.id} style={styles.frame}>
|
||||
{item.resultUrl && item.resultUrl.length > 0 ? (
|
||||
<Image
|
||||
source={{ uri: item.resultUrl[0] }}
|
||||
style={[styles.image, { height: getImageHeight(item.type) }]}
|
||||
/>
|
||||
) : (
|
||||
<View style={[styles.image, { height: getImageHeight(item.type) }, styles.placeholderImage]}>
|
||||
<Text style={styles.placeholderText}>
|
||||
{item.type === 'VIDEO' ? '🎬' : item.type === 'IMAGE' ? '🖼️' : '📝'}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
key={item.id}
|
||||
style={styles.frame}
|
||||
onPress={() => router.push(`/result?generationId=${item.id}`)}
|
||||
activeOpacity={0.9}
|
||||
>
|
||||
{renderMediaItem(item)}
|
||||
|
||||
<View style={styles.overlay}>
|
||||
<View style={styles.statusBadge}>
|
||||
@@ -238,7 +360,7 @@ export default function HistoryScreen() {
|
||||
</View>
|
||||
<Text style={styles.templateName}>{item.template?.title || '未知模板'}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
@@ -246,6 +368,19 @@ export default function HistoryScreen() {
|
||||
);
|
||||
})
|
||||
)}
|
||||
|
||||
{loadingMore && (
|
||||
<View style={styles.loadingMoreContainer}>
|
||||
<ActivityIndicator size="small" color="#FFFFFF" />
|
||||
<Text style={styles.loadingMoreText}>加载更多...</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{!hasMore && allGenerations.length > 0 && (
|
||||
<View style={styles.noMoreContainer}>
|
||||
<Text style={styles.noMoreText}>没有更多内容了</Text>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
</ThemedView>
|
||||
@@ -292,13 +427,13 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
frame: {
|
||||
width: '100%',
|
||||
borderRadius: 28,
|
||||
borderRadius: 16,
|
||||
marginBottom: 16,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: '#1A1A1A',
|
||||
},
|
||||
image: {
|
||||
width: '100%',
|
||||
borderRadius: 28,
|
||||
},
|
||||
placeholderImage: {
|
||||
backgroundColor: '#2A2A2A',
|
||||
@@ -308,15 +443,28 @@ const styles = StyleSheet.create({
|
||||
placeholderText: {
|
||||
fontSize: 48,
|
||||
},
|
||||
videoPlaceholder: {
|
||||
backgroundColor: '#1A1A1A',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
videoIcon: {
|
||||
fontSize: 48,
|
||||
marginBottom: 8,
|
||||
},
|
||||
videoText: {
|
||||
fontSize: 14,
|
||||
color: '#9E9E9E',
|
||||
},
|
||||
overlay: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
padding: 12,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.6)',
|
||||
borderBottomLeftRadius: 28,
|
||||
borderBottomRightRadius: 28,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.7)',
|
||||
borderBottomLeftRadius: 16,
|
||||
borderBottomRightRadius: 16,
|
||||
},
|
||||
statusBadge: {
|
||||
flexDirection: 'row',
|
||||
@@ -374,4 +522,23 @@ const styles = StyleSheet.create({
|
||||
dateGroup: {
|
||||
marginBottom: 24,
|
||||
},
|
||||
loadingMoreContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 20,
|
||||
gap: 12,
|
||||
},
|
||||
loadingMoreText: {
|
||||
fontSize: 14,
|
||||
color: '#9E9E9E',
|
||||
},
|
||||
noMoreContainer: {
|
||||
paddingVertical: 20,
|
||||
alignItems: 'center',
|
||||
},
|
||||
noMoreText: {
|
||||
fontSize: 14,
|
||||
color: '#666666',
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user