355 lines
8.7 KiB
TypeScript
355 lines
8.7 KiB
TypeScript
import { ErrorView } from '@/components/ErrorView';
|
|
import { PageLayout } from '@/components/bestai/layout';
|
|
import VideoPlayer from '@/components/sker/video-player/video-player';
|
|
import { getTemplateGenerations, TemplateGeneration } from '@/lib/api/template-generations';
|
|
import { groupByDate } from '@/lib/utils/date';
|
|
import { distributeToColumns } from '@/lib/utils/media';
|
|
import { FlashList } from '@shopify/flash-list';
|
|
import { router } from 'expo-router';
|
|
import { StatusBar } from 'expo-status-bar';
|
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
import {
|
|
ActivityIndicator,
|
|
StyleSheet,
|
|
Text,
|
|
TouchableOpacity,
|
|
View
|
|
} from 'react-native';
|
|
|
|
const LAYOUT_CONFIG = {
|
|
VIDEO_HEIGHT: 280,
|
|
IMAGE_HEIGHT: 240,
|
|
DEFAULT_HEIGHT: 200,
|
|
COLUMN_GAP: 16,
|
|
PAGE_SIZE: 20,
|
|
} as const;
|
|
|
|
const ColumnRenderer = React.memo(({ items }: { items: TemplateGeneration[] }) => (
|
|
<>
|
|
{items.map(item => {
|
|
const itemHeight = item.type === 'VIDEO' ? LAYOUT_CONFIG.VIDEO_HEIGHT : LAYOUT_CONFIG.IMAGE_HEIGHT;
|
|
return (
|
|
<TouchableOpacity
|
|
key={item.id}
|
|
style={[styles.frame, { height: itemHeight }]}
|
|
onPress={() => router.push(`/result?generationId=${item.id}`)}
|
|
activeOpacity={0.9}
|
|
>
|
|
{item.resultUrl.map(uri => {
|
|
return <VideoPlayer key={uri} source={{ uri, useCaching: true }} style={{ borderRadius: 8 }}/>
|
|
})}
|
|
</TouchableOpacity>
|
|
);
|
|
})}
|
|
</>
|
|
));
|
|
|
|
ColumnRenderer.displayName = 'ColumnRenderer';
|
|
|
|
interface DateGroupItem {
|
|
date: string;
|
|
items: TemplateGeneration[];
|
|
}
|
|
|
|
export default function HistoryScreen() {
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [page, setPage] = useState(1);
|
|
const [hasMore, setHasMore] = useState(true);
|
|
const [allGenerations, setAllGenerations] = useState<TemplateGeneration[]>([]);
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
|
|
const groupedData = useMemo(() => groupByDate(allGenerations), [allGenerations]);
|
|
const flatData = useMemo<DateGroupItem[]>(() =>
|
|
Object.entries(groupedData).map(([date, items]) => ({ date, items })),
|
|
[groupedData]
|
|
);
|
|
|
|
const fetchData = useCallback(async (pageNum: number = 1, isRefresh: boolean = false) => {
|
|
try {
|
|
if (isRefresh) {
|
|
setRefreshing(true);
|
|
} else if (pageNum === 1) {
|
|
setLoading(true);
|
|
}
|
|
|
|
const response = await getTemplateGenerations({
|
|
page: String(pageNum),
|
|
limit: String(LAYOUT_CONFIG.PAGE_SIZE)
|
|
});
|
|
|
|
if (response?.success && response.data) {
|
|
const newGenerations = response.data.generations as any[];
|
|
|
|
if (isRefresh || pageNum === 1) {
|
|
setAllGenerations(newGenerations);
|
|
setPage(1);
|
|
} else {
|
|
setAllGenerations(prev => [...prev, ...newGenerations]);
|
|
}
|
|
|
|
setHasMore(newGenerations.length === LAYOUT_CONFIG.PAGE_SIZE);
|
|
setError(null);
|
|
} else {
|
|
setError('获取数据失败');
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to fetch data:', err);
|
|
const errorMessage = err instanceof Error ? err.message : '获取数据失败';
|
|
if (!errorMessage.includes('401')) {
|
|
setError(errorMessage);
|
|
}
|
|
} finally {
|
|
setLoading(false);
|
|
setRefreshing(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
fetchData(1);
|
|
}, [fetchData]);
|
|
|
|
const onRefresh = useCallback(() => {
|
|
fetchData(1, true);
|
|
}, [fetchData]);
|
|
|
|
const loadMore = useCallback(() => {
|
|
if (hasMore && !loading && !refreshing) {
|
|
const nextPage = page + 1;
|
|
setPage(nextPage);
|
|
fetchData(nextPage, false);
|
|
}
|
|
}, [hasMore, page, loading, refreshing, fetchData]);
|
|
|
|
const renderItem = useCallback(({ item }: { item: DateGroupItem }) => {
|
|
const { leftColumn, rightColumn } = distributeToColumns(
|
|
item.items,
|
|
LAYOUT_CONFIG.VIDEO_HEIGHT,
|
|
LAYOUT_CONFIG.IMAGE_HEIGHT,
|
|
LAYOUT_CONFIG.COLUMN_GAP
|
|
);
|
|
|
|
return (
|
|
<View style={styles.dateGroup}>
|
|
<Text style={styles.dateline}>{item.date}</Text>
|
|
<View style={styles.gallery}>
|
|
<View style={styles.leadingLane}>
|
|
<ColumnRenderer items={leftColumn} />
|
|
</View>
|
|
<View style={styles.trailingLane}>
|
|
<ColumnRenderer items={rightColumn} />
|
|
</View>
|
|
</View>
|
|
</View>
|
|
);
|
|
}, []);
|
|
|
|
const ListHeaderComponent = useCallback(() => (
|
|
<Text style={styles.heading}>Content Generation</Text>
|
|
), []);
|
|
|
|
const ListFooterComponent = useCallback(() => {
|
|
if (hasMore) {
|
|
return (
|
|
<View style={styles.loadingMoreContainer}>
|
|
<ActivityIndicator size="small" color="#FFFFFF" />
|
|
<Text style={styles.loadingMoreText}>加载更多...</Text>
|
|
</View>
|
|
);
|
|
}
|
|
if (allGenerations.length > 0) {
|
|
return (
|
|
<View style={styles.noMoreContainer}>
|
|
<Text style={styles.noMoreText}>没有更多内容了</Text>
|
|
</View>
|
|
);
|
|
}
|
|
return null;
|
|
}, [hasMore, allGenerations.length]);
|
|
|
|
const ListEmptyComponent = useCallback(() => (
|
|
<View style={styles.emptyContainer}>
|
|
<Text style={styles.emptyText}>暂无生成记录</Text>
|
|
</View>
|
|
), []);
|
|
|
|
if (loading) {
|
|
return (
|
|
<PageLayout backgroundColor="#050505">
|
|
<StatusBar style="light" />
|
|
<View style={styles.loadingContainer}>
|
|
<ActivityIndicator size="large" color="#FFFFFF" />
|
|
<Text style={styles.loadingText}>加载中...</Text>
|
|
</View>
|
|
</PageLayout>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<PageLayout backgroundColor="#050505">
|
|
<StatusBar style="light" />
|
|
<ErrorView message={error} onRetry={() => fetchData(1)} />
|
|
</PageLayout>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<PageLayout backgroundColor="#050505">
|
|
<StatusBar style="light" />
|
|
<FlashList
|
|
data={flatData}
|
|
renderItem={renderItem}
|
|
keyExtractor={(item) => item.date}
|
|
onEndReached={loadMore}
|
|
onEndReachedThreshold={0.5}
|
|
refreshing={refreshing}
|
|
onRefresh={onRefresh}
|
|
ListHeaderComponent={ListHeaderComponent}
|
|
ListFooterComponent={ListFooterComponent}
|
|
ListEmptyComponent={ListEmptyComponent}
|
|
contentContainerStyle={styles.flashListContent}
|
|
/>
|
|
</PageLayout>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
flashListContent: {
|
|
paddingTop: 14,
|
|
paddingBottom: 14,
|
|
},
|
|
heading: {
|
|
fontSize: 24,
|
|
fontWeight: '600',
|
|
color: '#FFFFFF',
|
|
textAlign: 'center',
|
|
letterSpacing: 0.4,
|
|
marginBottom: 16,
|
|
},
|
|
dateline: {
|
|
marginTop: 16,
|
|
marginBottom: 16,
|
|
fontSize: 16,
|
|
fontWeight: '500',
|
|
color: '#EDEDED',
|
|
},
|
|
gallery: {
|
|
flexDirection: 'row',
|
|
marginBottom: 24,
|
|
},
|
|
leadingLane: {
|
|
flex: 1,
|
|
paddingRight: 12,
|
|
},
|
|
trailingLane: {
|
|
flex: 1,
|
|
paddingLeft: 12,
|
|
},
|
|
frame: {
|
|
width: '100%',
|
|
borderRadius: 16,
|
|
marginBottom: 16,
|
|
overflow: 'hidden',
|
|
backgroundColor: '#1A1A1A',
|
|
},
|
|
image: {
|
|
width: '100%',
|
|
},
|
|
placeholderImage: {
|
|
backgroundColor: '#2A2A2A',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
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.7)',
|
|
borderBottomLeftRadius: 16,
|
|
borderBottomRightRadius: 16,
|
|
},
|
|
statusBadge: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
marginBottom: 4,
|
|
},
|
|
statusDot: {
|
|
width: 8,
|
|
height: 8,
|
|
borderRadius: 4,
|
|
marginRight: 6,
|
|
},
|
|
statusText: {
|
|
fontSize: 12,
|
|
color: '#FFFFFF',
|
|
fontWeight: '500',
|
|
},
|
|
templateName: {
|
|
fontSize: 13,
|
|
color: '#FFFFFF',
|
|
fontWeight: '600',
|
|
},
|
|
loadingContainer: {
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
loadingText: {
|
|
marginTop: 16,
|
|
fontSize: 16,
|
|
color: '#FFFFFF',
|
|
},
|
|
emptyContainer: {
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
padding: 48,
|
|
},
|
|
emptyText: {
|
|
fontSize: 16,
|
|
color: '#9E9E9E',
|
|
textAlign: 'center',
|
|
},
|
|
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',
|
|
},
|
|
});
|