fix: 修复所有 TypeScript 类型错误

- 修复 webpPreviewUrl -> previewUrl 字段名错误
- 修复 video.tsx 中的 useTemplates 参数类型问题
- 修复 video.tsx 中 position 重复定义问题
- 修复 searchResults.tsx 中的 SearchResultItem 类型不匹配
- 修复 SearchResultsGrid.tsx 中不存在的 scrollContent 样式
- 修复 use-templates.ts 中 page 可能是 undefined 的类型问题
- 添加 tsconfig.json 的 exclude 配置
- 修复 use-template-actions.ts 中 null 不能赋值给 ApiError 的问题

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
imeepos
2026-01-16 12:14:04 +08:00
parent ae120f24d3
commit 4d1e901032
7 changed files with 406 additions and 343 deletions

View File

@@ -1,4 +1,4 @@
import { useState, useRef } from 'react'
import { useState, useRef, useEffect, useCallback, memo } from 'react'
import {
View,
Text,
@@ -7,176 +7,187 @@ import {
FlatList,
Pressable,
StatusBar as RNStatusBar,
ActivityIndicator,
RefreshControl,
} from 'react-native'
import { StatusBar } from 'expo-status-bar'
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
import { SafeAreaView } from 'react-native-safe-area-context'
import { Image, ImageLoadEventData } from 'expo-image'
import { useTranslation } from 'react-i18next'
import { SameStyleIcon, VideoIcon, WhiteStarIcon } from '@/components/icon'
import { SameStyleIcon, WhiteStarIcon } from '@/components/icon'
import { useRouter } from 'expo-router'
import { useTemplates, type TemplateDetail } from '@/hooks'
const { width: screenWidth, height: screenHeight } = Dimensions.get('window')
const TAB_BAR_HEIGHT = 83 // 底部导航栏高度
const TAB_BAR_HEIGHT = 83
// 视频数据
const videoData = [
{
id: 1,
videoUrl: require('@/assets/images/android-icon-background.png'),
thumbnailUrl: require('@/assets/images/android-icon-background.png'),
title: '雅琳圣诞写真',
duration: '00:03',
},
{
id: 2,
videoUrl: require('@/assets/images/android-icon-background.png'),
thumbnailUrl: require('@/assets/images/android-icon-background.png'),
title: '宠物写真',
duration: '00:05',
},
{
id: 3,
videoUrl: require('@/assets/images/android-icon-background.png'),
thumbnailUrl: require('@/assets/images/android-icon-background.png'),
title: '我和小猫的人生合照',
duration: '00:04',
},
]
// 统一的布局组件
const Layout = ({ children }: { children: React.ReactNode }) => (
<SafeAreaView style={styles.container} edges={[]}>
<StatusBar style="light" />
<RNStatusBar barStyle="light-content" />
{children}
</SafeAreaView>
)
interface VideoItemProps {
item: typeof videoData[0]
index: number
const LoadingState = () => (
<Layout>
<View style={styles.centerContainer}>
<ActivityIndicator size="large" color="#FFE500" />
<Text style={styles.messageText}>...</Text>
</View>
</Layout>
)
const ErrorState = () => (
<Layout>
<View style={styles.centerContainer}>
<Text style={styles.messageText}></Text>
</View>
</Layout>
)
const FooterLoading = () => (
<View style={styles.footerLoading}>
<ActivityIndicator size="small" color="#FFE500" />
</View>
)
// 计算图片显示尺寸
const calculateImageSize = (
imageSize: { width: number; height: number } | null,
videoHeight: number
) => {
if (!imageSize) return { width: screenWidth, height: videoHeight }
const imageAspectRatio = imageSize.width / imageSize.height
const screenAspectRatio = screenWidth / videoHeight
return imageAspectRatio > screenAspectRatio
? { width: screenWidth, height: screenWidth / imageAspectRatio }
: { width: videoHeight * imageAspectRatio, height: videoHeight }
}
function VideoItem({ item, videoHeight }: VideoItemProps) {
const VideoItem = memo(({ item, videoHeight }: { item: TemplateDetail; videoHeight: number }) => {
const { t } = useTranslation()
const router = useRouter()
const [isPlaying, setIsPlaying] = useState(false)
const [imageSize, setImageSize] = useState<{ width: number; height: number } | null>(null)
const handleImageLoad = (event: ImageLoadEventData) => {
const handleImageLoad = useCallback((event: ImageLoadEventData) => {
const { source } = event
if (source?.width && source?.height) {
setImageSize({ width: source.width, height: source.height })
}
}
}, [])
// 根据图片比例计算显示尺寸
const getImageStyle = () => {
if (!imageSize) {
return {
width: screenWidth,
height: videoHeight,
}
}
const handlePress = useCallback(() => {
router.push({
pathname: '/generateVideo' as any,
params: { template: JSON.stringify(item) },
})
}, [router, item])
const imageAspectRatio = imageSize.width / imageSize.height
const screenAspectRatio = screenWidth / videoHeight
let displayWidth = screenWidth
let displayHeight = videoHeight
if (imageAspectRatio > screenAspectRatio) {
// 图片更宽,以宽度为准
displayHeight = screenWidth / imageAspectRatio
} else {
// 图片更高,以高度为准
displayWidth = videoHeight * imageAspectRatio
}
return {
width: displayWidth,
height: displayHeight,
}
}
const imageStyle = calculateImageSize(imageSize, videoHeight)
return (
<View style={[styles.videoContainer, { height: videoHeight }]}>
{/* 主视频区域 */}
<View style={styles.videoWrapper}>
<Image
source={item.videoUrl}
style={[getImageStyle()]}
source={{ uri: item.previewUrl }}
style={imageStyle}
contentFit="contain"
onLoad={handleImageLoad}
/>
{/* 播放按钮 */}
{!isPlaying && (
<Pressable
style={styles.playButton}
onPress={() => setIsPlaying(true)}
>
<Pressable style={styles.playButton} onPress={() => setIsPlaying(true)}>
<View style={styles.playIcon}>
<View style={styles.playTriangle} />
</View>
</Pressable>
)}
{/* 左下角原图缩略图 */}
<View style={styles.thumbnailContainer}>
<Image
source={item.thumbnailUrl}
style={styles.thumbnail}
contentFit="cover"
/>
<Image source={{ uri: item.coverImageUrl }} style={styles.thumbnail} contentFit="cover" />
</View>
</View>
{/* 左下角按钮 */}
<Pressable style={styles.actionButtonLeft}>
<WhiteStarIcon />
<Text style={styles.actionButtonTextTitle}>{item.title}</Text>
</Pressable>
{/* 右下角按钮 */}
<Pressable
style={styles.actionButtonRight}
onPress={() => {
router.push({
pathname: '/generateVideo' as any,
params: {
template: JSON.stringify(item),
},
})
}}
>
<Pressable style={styles.actionButtonRight} onPress={handlePress}>
<SameStyleIcon />
<Text style={styles.actionButtonText}>{t('video.makeSame')}</Text>
</Pressable>
</View>
)
}
})
export default function VideoScreen() {
const flatListRef = useRef<FlatList>(null)
const insets = useSafeAreaInsets()
const videoHeight = screenHeight - TAB_BAR_HEIGHT
const { templates, loading, error, execute, refetch, loadMore, hasMore } = useTemplates({
sortBy: 'likeCount',
sortOrder: 'desc',
})
const [refreshing, setRefreshing] = useState(false)
useEffect(() => {
execute()
}, [execute])
const handleRefresh = useCallback(async () => {
setRefreshing(true)
await refetch()
setRefreshing(false)
}, [refetch])
const handleLoadMore = useCallback(() => {
if (!hasMore || loading) return
loadMore()
}, [hasMore, loading, loadMore])
const renderItem = useCallback(({ item }: { item: TemplateDetail }) => (
<VideoItem item={item} videoHeight={videoHeight} />
), [videoHeight])
const keyExtractor = useCallback((item: TemplateDetail) => item.id, [])
const getItemLayout = useCallback((_: any, index: number) => ({
length: videoHeight,
offset: videoHeight * index,
index,
}), [videoHeight])
if (loading && templates.length === 0) return <LoadingState />
if (error && templates.length === 0) return <ErrorState />
return (
<SafeAreaView style={styles.container} edges={[]}>
<StatusBar style="light" />
<RNStatusBar barStyle="light-content" />
<Layout>
<FlatList
ref={flatListRef}
data={videoData}
renderItem={({ item, index }) => (
<VideoItem item={item} index={index} videoHeight={videoHeight} />
)}
keyExtractor={(item) => item.id.toString()}
data={templates}
renderItem={renderItem}
keyExtractor={keyExtractor}
getItemLayout={getItemLayout}
pagingEnabled
showsVerticalScrollIndicator={false}
snapToInterval={videoHeight}
snapToAlignment="start"
decelerationRate="fast"
getItemLayout={(_, index) => ({
length: videoHeight,
offset: videoHeight * index,
index,
})}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={handleRefresh}
tintColor="#FFE500"
colors={['#FFE500']}
/>
}
onEndReached={handleLoadMore}
onEndReachedThreshold={0.5}
ListFooterComponent={loading ? <FooterLoading /> : null}
/>
</SafeAreaView>
</Layout>
)
}
@@ -185,24 +196,33 @@ const styles = StyleSheet.create({
flex: 1,
backgroundColor: '#090A0B',
},
centerContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
gap: 12,
},
messageText: {
color: '#FFFFFF',
fontSize: 14,
textAlign: 'center',
},
footerLoading: {
paddingVertical: 20,
alignItems: 'center',
},
videoContainer: {
width: screenWidth,
backgroundColor: '#090A0B',
position: 'relative',
},
videoWrapper: {
flex: 1,
position: 'relative',
backgroundColor: '#090A0B',
alignItems: 'center',
justifyContent: 'center',
},
playButton: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
...StyleSheet.absoluteFillObject,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.3)',
@@ -236,7 +256,6 @@ const styles = StyleSheet.create({
overflow: 'hidden',
borderWidth: 2,
borderColor: '#FFFFFF',
backgroundColor: '#090A0B',
},
thumbnail: {
width: '100%',