fix: bug
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { ThemedView } from '@/components/themed-view';
|
||||
import { PageLayout } from '@/components/bestai/layout';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Image,
|
||||
@@ -12,10 +12,19 @@ import {
|
||||
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';
|
||||
|
||||
const LAYOUT_CONFIG = {
|
||||
VIDEO_HEIGHT: 280,
|
||||
IMAGE_HEIGHT: 240,
|
||||
DEFAULT_HEIGHT: 200,
|
||||
COLUMN_GAP: 16,
|
||||
PAGE_SIZE: 20,
|
||||
LOAD_MORE_THRESHOLD: 200,
|
||||
SCROLL_THROTTLE: 800,
|
||||
} as const;
|
||||
|
||||
interface GroupedData {
|
||||
[date: string]: TemplateGeneration[];
|
||||
}
|
||||
@@ -86,14 +95,14 @@ const distributeToColumns = (items: TemplateGeneration[]) => {
|
||||
let rightHeight = 0;
|
||||
|
||||
items.forEach(item => {
|
||||
const height = item.type === 'VIDEO' ? 280 : 240;
|
||||
const height = item.type === 'VIDEO' ? LAYOUT_CONFIG.VIDEO_HEIGHT : LAYOUT_CONFIG.IMAGE_HEIGHT;
|
||||
|
||||
if (leftHeight <= rightHeight) {
|
||||
leftColumn.push(item);
|
||||
leftHeight += height + 16;
|
||||
leftHeight += height + LAYOUT_CONFIG.COLUMN_GAP;
|
||||
} else {
|
||||
rightColumn.push(item);
|
||||
rightHeight += height + 16;
|
||||
rightHeight += height + LAYOUT_CONFIG.COLUMN_GAP;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -103,11 +112,11 @@ const distributeToColumns = (items: TemplateGeneration[]) => {
|
||||
const getImageHeight = (type: string): number => {
|
||||
switch (type) {
|
||||
case 'VIDEO':
|
||||
return 280;
|
||||
return LAYOUT_CONFIG.VIDEO_HEIGHT;
|
||||
case 'IMAGE':
|
||||
return 240;
|
||||
return LAYOUT_CONFIG.IMAGE_HEIGHT;
|
||||
default:
|
||||
return 200;
|
||||
return LAYOUT_CONFIG.DEFAULT_HEIGHT;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -139,8 +148,88 @@ const getStatusText = (status: string): string => {
|
||||
}
|
||||
};
|
||||
|
||||
const MediaItem = React.memo(({ item }: { 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"
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
MediaItem.displayName = 'MediaItem';
|
||||
|
||||
const MediaOverlay = React.memo(({ item }: { item: TemplateGeneration }) => (
|
||||
<View style={styles.overlay}>
|
||||
<View style={styles.statusBadge}>
|
||||
<View style={[styles.statusDot, { backgroundColor: getStatusColor(item.status) }]} />
|
||||
<Text style={styles.statusText}>{getStatusText(item.status)}</Text>
|
||||
</View>
|
||||
<Text style={styles.templateName}>{item.template?.title || '未知模板'}</Text>
|
||||
</View>
|
||||
));
|
||||
|
||||
MediaOverlay.displayName = 'MediaOverlay';
|
||||
|
||||
const ColumnRenderer = React.memo(({ items }: { items: TemplateGeneration[] }) => (
|
||||
<>
|
||||
{items.map(item => (
|
||||
<TouchableOpacity
|
||||
key={item.id}
|
||||
style={styles.frame}
|
||||
onPress={() => router.push(`/result?generationId=${item.id}`)}
|
||||
activeOpacity={0.9}
|
||||
>
|
||||
<MediaItem item={item} />
|
||||
<MediaOverlay item={item} />
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</>
|
||||
));
|
||||
|
||||
ColumnRenderer.displayName = 'ColumnRenderer';
|
||||
|
||||
export default function HistoryScreen() {
|
||||
const [groupedData, setGroupedData] = useState<GroupedData>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
@@ -149,7 +238,9 @@ export default function HistoryScreen() {
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [allGenerations, setAllGenerations] = useState<TemplateGeneration[]>([]);
|
||||
|
||||
const fetchData = async (pageNum: number = 1, isRefresh: boolean = false) => {
|
||||
const groupedData = useMemo(() => groupByDate(allGenerations), [allGenerations]);
|
||||
|
||||
const fetchData = useCallback(async (pageNum: number = 1, isRefresh: boolean = false) => {
|
||||
try {
|
||||
if (isRefresh) {
|
||||
setRefreshing(true);
|
||||
@@ -159,7 +250,7 @@ export default function HistoryScreen() {
|
||||
setLoadingMore(true);
|
||||
}
|
||||
|
||||
const response = await getTemplateGenerations({ page: pageNum, limit: 20 });
|
||||
const response = await getTemplateGenerations({ page: pageNum, limit: LAYOUT_CONFIG.PAGE_SIZE });
|
||||
|
||||
if (response.success && response.data) {
|
||||
const newGenerations = response.data.generations;
|
||||
@@ -171,30 +262,32 @@ export default function HistoryScreen() {
|
||||
setAllGenerations(prev => [...prev, ...newGenerations]);
|
||||
}
|
||||
|
||||
setHasMore(newGenerations.length === 20);
|
||||
const grouped = groupByDate(isRefresh || pageNum === 1 ? newGenerations : [...allGenerations, ...newGenerations]);
|
||||
setGroupedData(grouped);
|
||||
setHasMore(newGenerations.length === LAYOUT_CONFIG.PAGE_SIZE);
|
||||
setError(null);
|
||||
} else {
|
||||
setError('获取数据失败');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch data:', err);
|
||||
setError(err instanceof Error ? err.message : '获取数据失败');
|
||||
const errorMessage = err instanceof Error ? err.message : '获取数据失败';
|
||||
// 401 错误由 AuthProvider 处理,不显示错误页面
|
||||
if (!errorMessage.includes('401')) {
|
||||
setError(errorMessage);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
setLoadingMore(false);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData(1);
|
||||
}, []);
|
||||
}, [fetchData]);
|
||||
|
||||
const onRefresh = useCallback(() => {
|
||||
fetchData(1, true);
|
||||
}, []);
|
||||
}, [fetchData]);
|
||||
|
||||
const loadMore = useCallback(() => {
|
||||
if (!loadingMore && hasMore && !loading) {
|
||||
@@ -202,113 +295,58 @@ export default function HistoryScreen() {
|
||||
setPage(nextPage);
|
||||
fetchData(nextPage, false);
|
||||
}
|
||||
}, [loadingMore, hasMore, page, loading]);
|
||||
}, [loadingMore, hasMore, page, loading, fetchData]);
|
||||
|
||||
const handleScroll = (event: any) => {
|
||||
const handleScroll = useCallback((event: any) => {
|
||||
const { layoutMeasurement, contentOffset, contentSize } = event.nativeEvent;
|
||||
const paddingToBottom = 20;
|
||||
const isCloseToBottom = layoutMeasurement.height + contentOffset.y >= contentSize.height - paddingToBottom;
|
||||
const isCloseToBottom = layoutMeasurement.height + contentOffset.y >= contentSize.height - LAYOUT_CONFIG.LOAD_MORE_THRESHOLD;
|
||||
|
||||
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"
|
||||
/>
|
||||
);
|
||||
};
|
||||
}, [loadingMore, hasMore, loadMore]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<ThemedView style={styles.canvas} lightColor="#050505" darkColor="#050505">
|
||||
<PageLayout backgroundColor="#050505">
|
||||
<StatusBar style="light" />
|
||||
<SafeAreaView style={styles.safeArea}>
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color="#FFFFFF" />
|
||||
<Text style={styles.loadingText}>加载中...</Text>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
</ThemedView>
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color="#FFFFFF" />
|
||||
<Text style={styles.loadingText}>加载中...</Text>
|
||||
</View>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<ThemedView style={styles.canvas} lightColor="#050505" darkColor="#050505">
|
||||
<PageLayout backgroundColor="#050505">
|
||||
<StatusBar style="light" />
|
||||
<SafeAreaView style={styles.safeArea}>
|
||||
<View style={styles.errorContainer}>
|
||||
<Text style={styles.errorText}>{error}</Text>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
</ThemedView>
|
||||
<View style={styles.errorContainer}>
|
||||
<Text style={styles.errorText}>{error}</Text>
|
||||
</View>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemedView style={styles.canvas} lightColor="#050505" darkColor="#050505">
|
||||
<PageLayout backgroundColor="#050505">
|
||||
<StatusBar style="light" />
|
||||
<SafeAreaView style={styles.safeArea} edges={['top', 'left', 'right']}>
|
||||
<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>
|
||||
<ScrollView
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={onRefresh}
|
||||
tintColor="#FFFFFF"
|
||||
colors={['#FFFFFF']}
|
||||
/>
|
||||
}
|
||||
onScroll={handleScroll}
|
||||
scrollEventThrottle={LAYOUT_CONFIG.SCROLL_THROTTLE}
|
||||
>
|
||||
<Text style={styles.heading}>Content Generation</Text>
|
||||
|
||||
{Object.keys(groupedData).length === 0 ? (
|
||||
<View style={styles.emptyContainer}>
|
||||
@@ -323,45 +361,11 @@ export default function HistoryScreen() {
|
||||
<Text style={styles.dateline}>{date}</Text>
|
||||
<View style={styles.gallery}>
|
||||
<View style={styles.leadingLane}>
|
||||
{leftColumn.map(item => (
|
||||
<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}>
|
||||
<View style={[styles.statusDot, { backgroundColor: getStatusColor(item.status) }]} />
|
||||
<Text style={styles.statusText}>{getStatusText(item.status)}</Text>
|
||||
</View>
|
||||
<Text style={styles.templateName}>{item.template?.title || '未知模板'}</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
<ColumnRenderer items={leftColumn} />
|
||||
</View>
|
||||
|
||||
<View style={styles.trailingLane}>
|
||||
{rightColumn.map(item => (
|
||||
<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}>
|
||||
<View style={[styles.statusDot, { backgroundColor: getStatusColor(item.status) }]} />
|
||||
<Text style={styles.statusText}>{getStatusText(item.status)}</Text>
|
||||
</View>
|
||||
<Text style={styles.templateName}>{item.template?.title || '未知模板'}</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
<ColumnRenderer items={rightColumn} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -376,26 +380,18 @@ export default function HistoryScreen() {
|
||||
</View>
|
||||
)}
|
||||
|
||||
{!hasMore && allGenerations.length > 0 && (
|
||||
<View style={styles.noMoreContainer}>
|
||||
<Text style={styles.noMoreText}>没有更多内容了</Text>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
</ThemedView>
|
||||
{!hasMore && allGenerations.length > 0 && (
|
||||
<View style={styles.noMoreContainer}>
|
||||
<Text style={styles.noMoreText}>没有更多内容了</Text>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
canvas: {
|
||||
flex: 1,
|
||||
},
|
||||
safeArea: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
paddingHorizontal: 24,
|
||||
paddingTop: 24,
|
||||
paddingBottom: 48,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user