fix: 修复布局问题

This commit is contained in:
imeepos
2025-11-12 10:02:55 +08:00
parent 26cd0139bf
commit a7d922b535
12 changed files with 876 additions and 699 deletions

View File

@@ -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',
},
});