fix: bug
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { Image } from 'expo-image'
|
||||
import { VideoView, useVideoPlayer } from 'expo-video'
|
||||
import { LinearGradient } from 'expo-linear-gradient'
|
||||
import { useRouter } from 'expo-router'
|
||||
import { useRouter, useLocalSearchParams } from 'expo-router'
|
||||
import { StatusBar } from 'expo-status-bar'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -23,10 +24,29 @@ import { useCategories } from '@/hooks/use-categories'
|
||||
|
||||
const { width: screenWidth } = Dimensions.get('window')
|
||||
|
||||
// 视频预览组件
|
||||
function VideoPreview({ source, style }: { source: string; style: any }) {
|
||||
const player = useVideoPlayer(source, (player) => {
|
||||
player.loop = true
|
||||
player.muted = true
|
||||
player.play()
|
||||
})
|
||||
|
||||
return (
|
||||
<VideoView
|
||||
style={style}
|
||||
player={player}
|
||||
allowsFullscreen
|
||||
allowsPictureInPicture
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default function HomeScreen() {
|
||||
const { t } = useTranslation()
|
||||
const insets = useSafeAreaInsets()
|
||||
const router = useRouter()
|
||||
const params = useLocalSearchParams()
|
||||
const [activeTab, setActiveTab] = useState(0)
|
||||
const [selectedCategoryId, setSelectedCategoryId] = useState<string | null>(null)
|
||||
|
||||
@@ -50,6 +70,19 @@ export default function HomeScreen() {
|
||||
}
|
||||
}, [categoriesData, categoriesError])
|
||||
|
||||
// 监听从 channels 页面传递过来的 categoryId 参数
|
||||
useEffect(() => {
|
||||
const categoryIdFromParams = params.categoryId as string | undefined
|
||||
if (categoryIdFromParams && categoriesData?.categories) {
|
||||
setSelectedCategoryId(categoryIdFromParams)
|
||||
// 同时更新 activeTab 索引
|
||||
const categoryIndex = categoriesData.categories.findIndex(cat => cat.id === categoryIdFromParams)
|
||||
if (categoryIndex !== -1) {
|
||||
setActiveTab(categoryIndex)
|
||||
}
|
||||
}
|
||||
}, [params.categoryId, categoriesData])
|
||||
|
||||
// 使用接口返回的分类数据,如果没有则使用默认翻译
|
||||
const categories = categoriesData?.categories || []
|
||||
const tabs = categories.length > 0
|
||||
@@ -69,17 +102,22 @@ export default function HomeScreen() {
|
||||
|
||||
// 将 CategoryTemplate 数据转换为卡片数据格式
|
||||
const displayCardData = categoryTemplates.length > 0
|
||||
? categoryTemplates.map((template, idx) => ({
|
||||
id: template.id || `template-${idx}`,
|
||||
title: template.title,
|
||||
image: { uri: template.previewUrl || template.coverImageUrl || `` },
|
||||
isHot: false,
|
||||
users: 0,
|
||||
height: undefined,
|
||||
previewUrl: template.previewUrl,
|
||||
aspectRatio: template.aspectRatio,
|
||||
tag: template.tag,
|
||||
}))
|
||||
? categoryTemplates.map((template, idx) => {
|
||||
const previewUrl = (template as any).webpPreviewUrl || template.previewUrl || template.coverImageUrl || ``
|
||||
const isVideo = previewUrl.includes('.mp4') || previewUrl.includes('.mov') || previewUrl.includes('.webm')
|
||||
return {
|
||||
id: template.id || `template-${idx}`,
|
||||
title: template.title,
|
||||
image: { uri: previewUrl },
|
||||
isHot: false,
|
||||
users: 0,
|
||||
height: undefined,
|
||||
previewUrl: template.previewUrl,
|
||||
aspectRatio: template.aspectRatio,
|
||||
tag: template.tag,
|
||||
isVideo,
|
||||
}
|
||||
})
|
||||
: []
|
||||
const [gridWidth, setGridWidth] = useState(screenWidth)
|
||||
const [showTabArrow, setShowTabArrow] = useState(false)
|
||||
@@ -164,7 +202,10 @@ export default function HomeScreen() {
|
||||
>
|
||||
<Pressable
|
||||
style={styles.tabArrow}
|
||||
onPress={() => router.push('/channels')}
|
||||
onPress={() => router.push({
|
||||
pathname: '/channels' as any,
|
||||
params: selectedCategoryId ? { categoryId: selectedCategoryId } : undefined,
|
||||
})}
|
||||
>
|
||||
<DownArrowIcon />
|
||||
</Pressable>
|
||||
@@ -321,11 +362,15 @@ export default function HomeScreen() {
|
||||
{ height: card.height || cardWidth * 1.2 },
|
||||
]}
|
||||
>
|
||||
<Image
|
||||
source={card.image}
|
||||
style={styles.cardImage}
|
||||
contentFit="cover"
|
||||
/>
|
||||
{card.isVideo ? (
|
||||
<VideoPreview source={card.image.uri} style={styles.cardImage} />
|
||||
) : (
|
||||
<Image
|
||||
source={card.image}
|
||||
style={styles.cardImage}
|
||||
contentFit="cover"
|
||||
/>
|
||||
)}
|
||||
<LinearGradient
|
||||
colors={['rgba(17, 17, 17, 0)', 'rgba(17, 17, 17, 0.9)']}
|
||||
start={{ x: 0, y: 0 }}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
StatusBar as RNStatusBar,
|
||||
ActivityIndicator,
|
||||
RefreshControl,
|
||||
Platform,
|
||||
} from 'react-native'
|
||||
import { StatusBar } from 'expo-status-bar'
|
||||
import { SafeAreaView } from 'react-native-safe-area-context'
|
||||
@@ -72,7 +73,6 @@ const calculateImageSize = (
|
||||
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 = useCallback((event: ImageLoadEventData) => {
|
||||
@@ -91,21 +91,19 @@ const VideoItem = memo(({ item, videoHeight }: { item: TemplateDetail; videoHeig
|
||||
|
||||
const imageStyle = calculateImageSize(imageSize, videoHeight)
|
||||
|
||||
// 优先使用 WebP 格式(支持动画),回退到普通预览图
|
||||
const displayUrl = item.webpPreviewUrl || item.previewUrl
|
||||
|
||||
return (
|
||||
<View style={[styles.videoContainer, { height: videoHeight }]}>
|
||||
<View style={styles.videoWrapper}>
|
||||
<Image
|
||||
source={{ uri: item.previewUrl }}
|
||||
style={imageStyle}
|
||||
contentFit="contain"
|
||||
onLoad={handleImageLoad}
|
||||
/>
|
||||
{!isPlaying && (
|
||||
<Pressable style={styles.playButton} onPress={() => setIsPlaying(true)}>
|
||||
<View style={styles.playIcon}>
|
||||
<View style={styles.playTriangle} />
|
||||
</View>
|
||||
</Pressable>
|
||||
{displayUrl && (
|
||||
<Image
|
||||
source={{ uri: displayUrl }}
|
||||
style={imageStyle}
|
||||
contentFit="contain"
|
||||
onLoad={handleImageLoad}
|
||||
/>
|
||||
)}
|
||||
<View style={styles.thumbnailContainer}>
|
||||
<Image source={{ uri: item.coverImageUrl }} style={styles.thumbnail} contentFit="cover" />
|
||||
@@ -126,11 +124,12 @@ const VideoItem = memo(({ item, videoHeight }: { item: TemplateDetail; videoHeig
|
||||
export default function VideoScreen() {
|
||||
const flatListRef = useRef<FlatList>(null)
|
||||
const videoHeight = screenHeight - TAB_BAR_HEIGHT
|
||||
const PAGE_SIZE = 10 // 每页10个数据
|
||||
const { templates, loading, error, execute, refetch, loadMore, hasMore } = useTemplates({
|
||||
sortBy: 'likeCount',
|
||||
sortOrder: 'desc',
|
||||
page: 1,
|
||||
limit: 20,
|
||||
limit: PAGE_SIZE,
|
||||
})
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
|
||||
@@ -145,9 +144,14 @@ export default function VideoScreen() {
|
||||
}, [refetch])
|
||||
|
||||
const handleLoadMore = useCallback(() => {
|
||||
if (!hasMore || loading) return
|
||||
console.log('onEndReached triggered', { hasMore, loading, templatesLength: templates.length })
|
||||
if (!hasMore || loading) {
|
||||
console.log('LoadMore blocked', { hasMore, loading })
|
||||
return
|
||||
}
|
||||
console.log('Loading more...')
|
||||
loadMore()
|
||||
}, [hasMore, loading, loadMore])
|
||||
}, [hasMore, loading, loadMore, templates.length])
|
||||
|
||||
const renderItem = useCallback(({ item }: { item: TemplateDetail }) => (
|
||||
<VideoItem item={item} videoHeight={videoHeight} />
|
||||
@@ -186,8 +190,11 @@ export default function VideoScreen() {
|
||||
/>
|
||||
}
|
||||
onEndReached={handleLoadMore}
|
||||
onEndReachedThreshold={0.5}
|
||||
onEndReachedThreshold={0.1}
|
||||
ListFooterComponent={loading ? <FooterLoading /> : null}
|
||||
maxToRenderPerBatch={5}
|
||||
windowSize={7}
|
||||
initialNumToRender={3}
|
||||
/>
|
||||
</Layout>
|
||||
)
|
||||
@@ -223,31 +230,6 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
playButton: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
||||
},
|
||||
playIcon: {
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 28,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.85)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
playTriangle: {
|
||||
width: 0,
|
||||
height: 0,
|
||||
borderLeftWidth: 18,
|
||||
borderTopWidth: 11,
|
||||
borderBottomWidth: 11,
|
||||
borderLeftColor: '#000000',
|
||||
borderTopColor: 'transparent',
|
||||
borderBottomColor: 'transparent',
|
||||
marginLeft: 3,
|
||||
},
|
||||
thumbnailContainer: {
|
||||
position: 'absolute',
|
||||
left: 12,
|
||||
|
||||
160
app/channels.tsx
160
app/channels.tsx
@@ -1,32 +1,46 @@
|
||||
import { useState } from 'react'
|
||||
import { useState, useEffect } from 'react'
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
Pressable,
|
||||
StatusBar as RNStatusBar,
|
||||
ActivityIndicator,
|
||||
} from 'react-native'
|
||||
import { StatusBar } from 'expo-status-bar'
|
||||
import { LinearGradient } from 'expo-linear-gradient'
|
||||
import { SafeAreaView } from 'react-native-safe-area-context'
|
||||
import { useRouter } from 'expo-router'
|
||||
import { useRouter, useLocalSearchParams } from 'expo-router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { TopArrowIcon } from '@/components/icon'
|
||||
import GradientText from '@/components/GradientText'
|
||||
import { useCategories } from '@/hooks/use-categories'
|
||||
|
||||
|
||||
export default function ChannelsScreen() {
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
const params = useLocalSearchParams()
|
||||
const [isExpanded, setIsExpanded] = useState(true)
|
||||
const [selectedChannel, setSelectedChannel] = useState(2) // 默认选中第二个频道
|
||||
|
||||
// 频道数据
|
||||
const channels = Array.from({ length: 13 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
name: t('channels.channelName'),
|
||||
}))
|
||||
|
||||
// 使用 useCategories hook 获取分类数据
|
||||
const { load, data: categoriesData, loading: categoriesLoading, error: categoriesError } = useCategories()
|
||||
|
||||
// 从路由参数获取当前选中的分类 ID
|
||||
const currentCategoryId = params.categoryId as string | undefined
|
||||
|
||||
useEffect(() => {
|
||||
// 加载分类数据
|
||||
load()
|
||||
}, [])
|
||||
|
||||
// 使用接口返回的分类数据
|
||||
const categories = categoriesData?.categories || []
|
||||
|
||||
// 如果没有分类数据,显示加载状态或空状态
|
||||
const showLoading = categoriesLoading
|
||||
const showEmptyState = !categoriesLoading && categories.length === 0
|
||||
|
||||
return (
|
||||
<View style={styles.screen}>
|
||||
@@ -41,11 +55,7 @@ export default function ChannelsScreen() {
|
||||
<Text style={styles.headerTitle}>{t('channels.title')}</Text>
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
if (isExpanded) {
|
||||
router.push('/')
|
||||
} else {
|
||||
setIsExpanded(true)
|
||||
}
|
||||
router.back()
|
||||
}}
|
||||
>
|
||||
<TopArrowIcon />
|
||||
@@ -55,45 +65,65 @@ export default function ChannelsScreen() {
|
||||
{/* 频道选择区域 */}
|
||||
{isExpanded && (
|
||||
<View style={styles.channelsSection}>
|
||||
<View style={styles.channelsGrid}>
|
||||
{channels.map((channel) => {
|
||||
return (
|
||||
<Pressable
|
||||
key={channel.id}
|
||||
onPress={() => setSelectedChannel(channel.id)}
|
||||
style={[styles.channelButtonWrapper]}
|
||||
>
|
||||
{selectedChannel === channel.id ? (
|
||||
<LinearGradient
|
||||
colors={['#FF9966', '#FF6699', '#9966FF']}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 0 }}
|
||||
style={styles.channelButtonGradient}
|
||||
>
|
||||
<View style={styles.channelButtonDefault}>
|
||||
<GradientText
|
||||
colors={['#FF9966', '#FF6699', '#9966FF']}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 0 }}
|
||||
style={styles.channelButtonText}
|
||||
>
|
||||
{channel.name}
|
||||
</GradientText>
|
||||
{showLoading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="small" color="#FF6699" />
|
||||
</View>
|
||||
) : showEmptyState ? (
|
||||
<View style={styles.emptyStateContainer}>
|
||||
<Text style={styles.emptyStateText}>{t('channels.noCategories') || '暂无分类'}</Text>
|
||||
<Pressable style={styles.retryButton} onPress={() => load()}>
|
||||
<Text style={styles.retryButtonText}>{t('channels.retry') || '重新加载'}</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.channelsGrid}>
|
||||
{categories.map((category) => {
|
||||
const isSelected = currentCategoryId === category.id
|
||||
return (
|
||||
<Pressable
|
||||
key={category.id}
|
||||
onPress={() => {
|
||||
// 选择分类后返回首页
|
||||
router.push({
|
||||
pathname: '/' as any,
|
||||
params: { categoryId: category.id },
|
||||
})
|
||||
}}
|
||||
style={[styles.channelButtonWrapper]}
|
||||
>
|
||||
{isSelected ? (
|
||||
<LinearGradient
|
||||
colors={['#FF9966', '#FF6699', '#9966FF']}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 0 }}
|
||||
style={styles.channelButtonGradient}
|
||||
>
|
||||
<View style={styles.channelButtonDefault}>
|
||||
<GradientText
|
||||
colors={['#FF9966', '#FF6699', '#9966FF']}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 0 }}
|
||||
style={styles.channelButtonText}
|
||||
>
|
||||
{category.name}
|
||||
</GradientText>
|
||||
</View>
|
||||
</LinearGradient>
|
||||
) : (
|
||||
<View style={styles.channelButtonWrapperUnselected}>
|
||||
<View style={styles.channelButtonDefault}>
|
||||
<Text style={styles.channelButtonText}>
|
||||
{category.name}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</LinearGradient>
|
||||
) : (
|
||||
<View style={styles.channelButtonWrapperUnselected}>
|
||||
<View style={styles.channelButtonDefault}>
|
||||
<Text style={styles.channelButtonText}>
|
||||
{channel.name}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</Pressable>
|
||||
)
|
||||
})}
|
||||
</View>
|
||||
)}
|
||||
</Pressable>
|
||||
)
|
||||
})}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</SafeAreaView>
|
||||
@@ -169,5 +199,31 @@ const styles = StyleSheet.create({
|
||||
fontWeight: '500',
|
||||
textAlign: 'center',
|
||||
},
|
||||
loadingContainer: {
|
||||
paddingVertical: 24,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
emptyStateContainer: {
|
||||
paddingVertical: 40,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 16,
|
||||
},
|
||||
emptyStateText: {
|
||||
color: '#ABABAB',
|
||||
fontSize: 14,
|
||||
},
|
||||
retryButton: {
|
||||
backgroundColor: '#FF6699',
|
||||
paddingHorizontal: 24,
|
||||
paddingVertical: 12,
|
||||
borderRadius: 8,
|
||||
},
|
||||
retryButtonText: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user