expo-popcore-app/app/likes.tsx

290 lines
8.9 KiB
TypeScript

import { useRouter } from 'expo-router'
import { StatusBar } from 'expo-status-bar'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import {
FlatList,
StyleSheet,
StatusBar as RNStatusBar,
View,
ActivityIndicator,
RefreshControl,
Dimensions,
Platform,
} from 'react-native'
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
import { TemplateCard } from '@/components/blocks/home'
import ErrorState from '@/components/ErrorState'
import LoadingState from '@/components/LoadingState'
import { useUserLikes } from '@/hooks'
import { useTemplateSocialStore } from '@/stores/templateSocialStore'
import { root } from '@repo/core'
import { TemplateSocialController } from '@repo/sdk'
import { handleError } from '@/hooks/use-error'
const NUM_COLUMNS = 2
const HORIZONTAL_PADDING = 16
const CARD_GAP = 5
const SCREEN_WIDTH = Dimensions.get('window').width
// 计算卡片宽度
const calculateCardWidth = () => {
return (SCREEN_WIDTH - HORIZONTAL_PADDING * 2 - CARD_GAP * (NUM_COLUMNS - 1)) / NUM_COLUMNS
}
const CARD_WIDTH = calculateCardWidth()
export default function LikesScreen() {
const insets = useSafeAreaInsets()
const router = useRouter()
const [refreshing, setRefreshing] = useState(false)
// 使用 Store 中的点赞/收藏状态
const { setLiked, setFavorited, incrementLikeCount, decrementLikeCount } = useTemplateSocialStore()
// 获取用户点赞列表
const {
likes,
loading,
loadingMore,
error,
execute,
refetch,
loadMore,
hasMore,
} = useUserLikes()
// 初始化加载
useEffect(() => {
execute()
}, [])
// 下拉刷新处理
const handleRefresh = useCallback(async () => {
setRefreshing(true)
await refetch()
setRefreshing(false)
}, [refetch])
// 加载更多处理
const handleEndReached = useCallback(() => {
if (!loadingMore && hasMore && !loading) {
loadMore()
}
}, [loadingMore, hasMore, loading, loadMore])
// 导航到模板详情
const handleTemplatePress = useCallback((id: string) => {
router.push({
pathname: '/templateDetail' as any,
params: { id },
})
}, [router])
// 获取 social controller
const getSocialController = useCallback(() => {
return root.get(TemplateSocialController)
}, [])
// 点赞/取消点赞处理
const handleLike = useCallback(async (id: string) => {
setLiked(id, true)
incrementLikeCount(id)
try {
const social = getSocialController()
await handleError(() => social.like({ templateId: id }))
} catch (e) {
setLiked(id, false)
decrementLikeCount(id)
}
}, [setLiked, incrementLikeCount, decrementLikeCount, getSocialController])
const handleUnlike = useCallback(async (id: string) => {
setLiked(id, false)
decrementLikeCount(id)
try {
const social = getSocialController()
await handleError(() => social.unlike({ templateId: id }))
} catch (e) {
setLiked(id, true)
incrementLikeCount(id)
}
}, [setLiked, incrementLikeCount, decrementLikeCount, getSocialController])
// 收藏/取消收藏处理
const handleFavorite = useCallback(async (id: string) => {
setFavorited(id, true)
try {
const social = getSocialController()
await handleError(() => social.favorite({ templateId: id }))
} catch (e) {
setFavorited(id, false)
}
}, [setFavorited, getSocialController])
const handleUnfavorite = useCallback(async (id: string) => {
setFavorited(id, false)
try {
const social = getSocialController()
await handleError(() => social.unfavorite({ templateId: id }))
} catch (e) {
setFavorited(id, true)
}
}, [setFavorited, getSocialController])
// 状态判断
const showEmptyState = useMemo(() =>
!loading && !error && likes.length === 0,
[loading, error, likes.length]
)
const showErrorState = useMemo(() =>
!loading && !!error,
[loading, error]
)
const showTemplateList = useMemo(() =>
!loading && !error && likes.length > 0,
[loading, error, likes.length]
)
// 渲染模板卡片
const renderTemplateItem = useCallback(({ item }: { item: typeof likes[0] }) => {
if (!item.template?.id) return null
return (
<TemplateCard
id={item.template.id}
title={item.template.title || ''}
titleEn={item.template.titleEn}
previewUrl={item.template.previewUrl}
webpPreviewUrl={item.template.webpPreviewUrl}
coverImageUrl={item.template.coverImageUrl}
aspectRatio={item.template.aspectRatio}
cardWidth={CARD_WIDTH}
onPress={handleTemplatePress}
liked={item.template.isLiked}
favorited={item.template.isFavorited}
likeCount={item.template.likeCount}
onLike={handleLike}
onUnlike={handleUnlike}
onFavorite={handleFavorite}
onUnfavorite={handleUnfavorite}
/>
)
}, [handleTemplatePress, handleLike, handleUnlike, handleFavorite, handleUnfavorite])
// 提取 key
const keyExtractor = useCallback((item: typeof likes[0]) => item.id || '', [])
// 列表头部组件
const ListHeaderComponent = useMemo(() => {
// 加载状态
if (loading) {
return <LoadingState />
}
// 错误状态
if (showErrorState) {
return (
<ErrorState
message="加载失败,请下拉刷新重试"
onRetry={() => refetch()}
variant="error"
/>
)
}
// 空状态
if (showEmptyState) {
return (
<ErrorState
message="暂无点赞"
onRetry={() => execute()}
/>
)
}
return null
}, [loading, showErrorState, showEmptyState, refetch, execute])
// 列表底部组件
const ListFooterComponent = useMemo(() => {
if (loadingMore) {
return (
<View style={styles.loadingMoreContainer} testID="loading-more">
<ActivityIndicator size="small" color="#FFFFFF" />
</View>
)
}
return null
}, [loadingMore])
// 获取有效的点赞列表(过滤掉没有 template.id 的)
const validLikes = useMemo(() =>
likes.filter(item => !!item.template?.id),
[likes]
)
return (
<SafeAreaView style={styles.container} edges={['top']}>
<StatusBar style="light" />
<RNStatusBar barStyle="light-content" />
<FlatList
data={showTemplateList ? validLikes : []}
keyExtractor={keyExtractor}
renderItem={renderTemplateItem}
numColumns={NUM_COLUMNS}
columnWrapperStyle={styles.columnWrapper}
style={styles.scrollView}
contentContainerStyle={[styles.scrollContent, { paddingBottom: 60 + insets.bottom + 20 }]}
showsVerticalScrollIndicator={false}
onEndReached={handleEndReached}
onEndReachedThreshold={0.5}
ListHeaderComponent={ListHeaderComponent}
ListFooterComponent={ListFooterComponent}
removeClippedSubviews={Platform.OS === 'android'}
maxToRenderPerBatch={12}
windowSize={5}
initialNumToRender={12}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={handleRefresh}
tintColor="#9966FF"
colors={['#9966FF', '#FF6699', '#FF9966']}
progressBackgroundColor="#1C1E22"
progressViewOffset={10}
/>
}
/>
</SafeAreaView>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#090A0B',
},
scrollView: {
flex: 1,
backgroundColor: '#090A0B',
},
scrollContent: {
paddingHorizontal: HORIZONTAL_PADDING,
paddingBottom: 20,
backgroundColor: '#090A0B',
},
columnWrapper: {
gap: CARD_GAP,
marginBottom: CARD_GAP,
},
loadingMoreContainer: {
paddingVertical: 20,
alignItems: 'center',
justifyContent: 'center',
},
})