diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index 448b0d0..189e320 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -25,6 +25,9 @@ import { useTemplateFilter } from '@/hooks/use-template-filter' import { useUserBalance } from '@/hooks/use-user-balance' import { useTemplateLike, useTemplateFavorite } 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 = 3 const HORIZONTAL_PADDING = 16 @@ -108,6 +111,21 @@ export default function HomeScreen() { } }, [params.categoryId]) + // 同步模板的点赞数量到 store(用于初始化 store 中的数据) + useEffect(() => { + if (filteredTemplates.length > 0) { + const likeCountMap: Record = {} + filteredTemplates.forEach(template => { + if (template.id && 'likeCount' in template && template.likeCount !== undefined) { + likeCountMap[template.id] = template.likeCount + } + }) + if (Object.keys(likeCountMap).length > 0) { + setLikeCountStates(likeCountMap) + } + } + }, [filteredTemplates, setLikeCountStates]) + // 状态判断 - 使用 useMemo 缓存 // 统一 loading 状态:只要有任何一个在加载,就显示 loading const isLoading = useMemo(() => @@ -154,35 +172,72 @@ export default function HomeScreen() { }), [router]) // 使用 Store 中的点赞/收藏状态 - const { setLiked, setFavorited } = useTemplateSocialStore() + const { setLiked, setFavorited, incrementLikeCount, decrementLikeCount, setLikeCountStates } = useTemplateSocialStore() + + // 获取 social controller + const getSocialController = useCallback(() => { + return root.get(TemplateSocialController) + }, []) // 点赞处理 - const handleLike = useCallback((id: string) => { - const currentState = setLiked(id, true) // 乐观更新 - // TODO: 调用 API - console.log('Like template:', id) - }, [setLiked]) + const handleLike = useCallback(async (id: string) => { + setLiked(id, true) // 乐观更新状态 + incrementLikeCount(id) // 乐观更新数量 + try { + const social = getSocialController() + await handleError(() => social.like({ templateId: id })) + console.log('Liked template:', id) + } catch (e) { + // 失败时回滚状态 + setLiked(id, false) + decrementLikeCount(id) + console.error('Like failed:', e) + } + }, [setLiked, incrementLikeCount, decrementLikeCount, getSocialController]) // 取消点赞处理 - const handleUnlike = useCallback((id: string) => { - const currentState = setLiked(id, false) // 乐观更新 - // TODO: 调用 API - console.log('Unlike template:', id) - }, [setLiked]) + const handleUnlike = useCallback(async (id: string) => { + setLiked(id, false) // 乐观更新状态 + decrementLikeCount(id) // 乐观更新数量 + try { + const social = getSocialController() + await handleError(() => social.unlike({ templateId: id })) + console.log('Unliked template:', id) + } catch (e) { + // 失败时回滚状态 + setLiked(id, true) + incrementLikeCount(id) + console.error('Unlike failed:', e) + } + }, [setLiked, incrementLikeCount, decrementLikeCount, getSocialController]) // 收藏处理 - const handleFavorite = useCallback((id: string) => { - const currentState = setFavorited(id, true) // 乐观更新 - // TODO: 调用 API - console.log('Favorite template:', id) - }, [setFavorited]) + const handleFavorite = useCallback(async (id: string) => { + setFavorited(id, true) // 乐观更新 + try { + const social = getSocialController() + await handleError(() => social.favorite({ templateId: id })) + console.log('Favorited template:', id) + } catch (e) { + // 失败时回滚状态 + setFavorited(id, false) + console.error('Favorite failed:', e) + } + }, [setFavorited, getSocialController]) // 取消收藏处理 - const handleUnfavorite = useCallback((id: string) => { - const currentState = setFavorited(id, false) // 乐观更新 - // TODO: 调用 API - console.log('Unfavorite template:', id) - }, [setFavorited]) + const handleUnfavorite = useCallback(async (id: string) => { + setFavorited(id, false) // 乐观更新 + try { + const social = getSocialController() + await handleError(() => social.unfavorite({ templateId: id })) + console.log('Unfavorited template:', id) + } catch (e) { + // 失败时回滚状态 + setFavorited(id, true) + console.error('Unfavorite failed:', e) + } + }, [setFavorited, getSocialController]) // 渲染模板卡片 const renderTemplateItem = useCallback(({ item }: { item: typeof filteredTemplates[0] }) => { @@ -200,6 +255,7 @@ export default function HomeScreen() { onPress={handleTemplatePress} liked={'isLiked' in item ? item.isLiked : undefined} favorited={'isFavorited' in item ? item.isFavorited : undefined} + likeCount={'likeCount' in item ? item.likeCount : undefined} onLike={handleLike} onUnlike={handleUnlike} onFavorite={handleFavorite} diff --git a/app/favorites.tsx b/app/favorites.tsx index c5aa500..2d32cc6 100644 --- a/app/favorites.tsx +++ b/app/favorites.tsx @@ -18,6 +18,9 @@ import ErrorState from '@/components/ErrorState' import LoadingState from '@/components/LoadingState' import { useUserFavorites } from '@/hooks/use-user-favorites' 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 @@ -36,7 +39,7 @@ export default function FavoritesScreen() { const router = useRouter() // 使用 Store 中的点赞/收藏状态 - const { setLiked, setFavorited } = useTemplateSocialStore() + const { setLiked, setFavorited, incrementLikeCount, decrementLikeCount } = useTemplateSocialStore() // 获取收藏列表 const { @@ -86,27 +89,56 @@ export default function FavoritesScreen() { }) }, [router]) - // 点赞/取消点赞处理 - const handleLike = useCallback((id: string) => { - setLiked(id, true) - console.log('Like template:', id) - }, [setLiked]) + // 获取 social controller + const getSocialController = useCallback(() => { + return root.get(TemplateSocialController) + }, []) - const handleUnlike = useCallback((id: string) => { + // 点赞/取消点赞处理 + 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) - console.log('Unlike template:', id) - }, [setLiked]) + 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((id: string) => { + const handleFavorite = useCallback(async (id: string) => { setFavorited(id, true) - console.log('Favorite template:', id) - }, [setFavorited]) + try { + const social = getSocialController() + await handleError(() => social.favorite({ templateId: id })) + } catch (e) { + setFavorited(id, false) + } + }, [setFavorited, getSocialController]) - const handleUnfavorite = useCallback((id: string) => { + const handleUnfavorite = useCallback(async (id: string) => { setFavorited(id, false) - console.log('Unfavorite template:', id) - }, [setFavorited]) + try { + const social = getSocialController() + await handleError(() => social.unfavorite({ templateId: id })) + } catch (e) { + setFavorited(id, true) + } + }, [setFavorited, getSocialController]) // 状态判断 const isLoading = useMemo(() => loading, [loading]) @@ -137,6 +169,7 @@ export default function FavoritesScreen() { onPress={handleTemplatePress} liked={template.isLiked} favorited={template.isFavorited} + likeCount={template.likeCount} onLike={handleLike} onUnlike={handleUnlike} onFavorite={handleFavorite} diff --git a/app/likes.tsx b/app/likes.tsx index df1d65c..4e7eee4 100644 --- a/app/likes.tsx +++ b/app/likes.tsx @@ -18,6 +18,9 @@ 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 @@ -37,7 +40,7 @@ export default function LikesScreen() { const [refreshing, setRefreshing] = useState(false) // 使用 Store 中的点赞/收藏状态 - const { setLiked, setFavorited } = useTemplateSocialStore() + const { setLiked, setFavorited, incrementLikeCount, decrementLikeCount } = useTemplateSocialStore() // 获取用户点赞列表 const { @@ -78,27 +81,56 @@ export default function LikesScreen() { }) }, [router]) - // 点赞/取消点赞处理 - const handleLike = useCallback((id: string) => { - setLiked(id, true) - console.log('Like template:', id) - }, [setLiked]) + // 获取 social controller + const getSocialController = useCallback(() => { + return root.get(TemplateSocialController) + }, []) - const handleUnlike = useCallback((id: string) => { + // 点赞/取消点赞处理 + 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) - console.log('Unlike template:', id) - }, [setLiked]) + 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((id: string) => { + const handleFavorite = useCallback(async (id: string) => { setFavorited(id, true) - console.log('Favorite template:', id) - }, [setFavorited]) + try { + const social = getSocialController() + await handleError(() => social.favorite({ templateId: id })) + } catch (e) { + setFavorited(id, false) + } + }, [setFavorited, getSocialController]) - const handleUnfavorite = useCallback((id: string) => { + const handleUnfavorite = useCallback(async (id: string) => { setFavorited(id, false) - console.log('Unfavorite template:', id) - }, [setFavorited]) + try { + const social = getSocialController() + await handleError(() => social.unfavorite({ templateId: id })) + } catch (e) { + setFavorited(id, true) + } + }, [setFavorited, getSocialController]) // 状态判断 const showEmptyState = useMemo(() => @@ -133,6 +165,7 @@ export default function LikesScreen() { onPress={handleTemplatePress} liked={item.template.isLiked} favorited={item.template.isFavorited} + likeCount={item.template.likeCount} onLike={handleLike} onUnlike={handleUnlike} onFavorite={handleFavorite} diff --git a/components/blocks/home/TemplateCard.tsx b/components/blocks/home/TemplateCard.tsx index 5718cd4..5042dc9 100644 --- a/components/blocks/home/TemplateCard.tsx +++ b/components/blocks/home/TemplateCard.tsx @@ -4,7 +4,7 @@ import { Image } from 'expo-image' import { LinearGradient } from 'expo-linear-gradient' import { Ionicons } from '@expo/vector-icons' import { useTranslation } from 'react-i18next' -import { useTemplateSocialStore } from '@/stores/templateSocialStore' +import { useTemplateSocialStore, useTemplateLiked, useTemplateFavorited, useTemplateLikeCount } from '@/stores/templateSocialStore' export interface TemplateCardProps { id?: string @@ -18,6 +18,7 @@ export interface TemplateCardProps { onPress: (id: string) => void liked?: boolean favorited?: boolean + likeCount?: number onLike?: (id: string) => void onUnlike?: (id: string) => void onFavorite?: (id: string) => void @@ -51,6 +52,23 @@ export function getImageUri( return webpPreviewUrl || previewUrl || coverImageUrl } +/** + * 格式化数字显示 + * 1000以下: 显示原数字 + * 1000-9999: 1.2k + * 10000-99999: 1w + * 100000以上: 10w + */ +export function formatCount(count: number): string { + if (count < 1000) { + return count.toString() + } + if (count < 10000) { + return `${(count / 1000).toFixed(1).replace(/\.0$/, '')}k` + } + return `${(count / 10000).toFixed(1).replace(/\.0$/, '')}w` +} + // 渐变颜色常量,避免每次渲染创建新数组 const GRADIENT_COLORS = ['rgba(17, 17, 17, 0)', 'rgba(17, 17, 17, 0.9)'] as const const GRADIENT_START = { x: 0, y: 0 } @@ -68,6 +86,7 @@ const TemplateCardComponent: React.FC = ({ onPress, liked: likedProp, favorited: favoritedProp, + likeCount: likeCountProp, onLike, onUnlike, onFavorite, @@ -76,12 +95,16 @@ const TemplateCardComponent: React.FC = ({ }) => { const { i18n } = useTranslation() - // 获取 Store 中的状态(用于本地状态覆盖) - const { isLiked: isLikedInStore, isFavorited: isFavoritedInStore } = useTemplateSocialStore() + // 订阅特定模板的点赞/收藏状态和点赞数量 + // 当该模板的状态变化时才会触发重新渲染,不影响其他卡片 + const storeLiked = useTemplateLiked(id) + const storeFavorited = useTemplateFavorited(id) + const storeLikeCount = useTemplateLikeCount(id) // 合并 props 状态和 store 状态:store 优先(乐观更新) - const liked = id !== undefined ? isLikedInStore(id) ?? likedProp : likedProp - const favorited = id !== undefined ? isFavoritedInStore(id) ?? favoritedProp : favoritedProp + const liked = storeLiked ?? likedProp + const favorited = storeFavorited ?? favoritedProp + const likeCount = storeLikeCount ?? likeCountProp const aspectRatio = useMemo(() => parseAspectRatio(aspectRatioString), [aspectRatioString]) const imageUri = useMemo(() => getImageUri(webpPreviewUrl, previewUrl, coverImageUrl), [webpPreviewUrl, previewUrl, coverImageUrl]) @@ -179,7 +202,12 @@ const TemplateCardComponent: React.FC = ({ hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }} testID={`${testID}-like-button`} > - + + + {likeCount !== undefined && likeCount > 0 && ( + {formatCount(likeCount)} + )} + { @@ -196,7 +224,12 @@ const TemplateCardComponent: React.FC = ({ ) : ( // 没有回调时显示为静态图标 - + + + {likeCount !== undefined && likeCount > 0 && ( + {formatCount(likeCount)} + )} + )} @@ -238,6 +271,11 @@ const styles = StyleSheet.create({ flexDirection: 'row', gap: 4, }, + likeButtonContainer: { + flexDirection: 'row', + alignItems: 'center', + gap: 2, + }, iconButton: { padding: 4, }, @@ -248,6 +286,15 @@ const styles = StyleSheet.create({ shadowRadius: 2, elevation: 2, }, + likeCount: { + fontSize: 10, + fontWeight: '600', + color: '#F5F5F5', + textShadowColor: 'rgba(0, 0, 0, 0.5)', + textShadowOffset: { width: 0, height: 1 }, + textShadowRadius: 2, + minWidth: 12, + }, cardTitle: { position: 'absolute', bottom: 8, diff --git a/stores/templateSocialStore.ts b/stores/templateSocialStore.ts index f50da2c..1751a82 100644 --- a/stores/templateSocialStore.ts +++ b/stores/templateSocialStore.ts @@ -1,4 +1,5 @@ import { create } from 'zustand' +import { devtools } from 'zustand/middleware' interface TemplateSocialState { // 点赞状态缓存: templateId -> boolean @@ -7,70 +8,166 @@ interface TemplateSocialState { // 收藏状态缓存: templateId -> boolean favoritedMap: Record + // 点赞数量缓存: templateId -> number + likeCountMap: Record + // 批量更新点赞状态 setLikedStates: (states: Record) => void // 批量更新收藏状态 setFavoritedStates: (states: Record) => void + // 批量更新点赞数量 + setLikeCountStates: (states: Record) => void + // 更新单个点赞状态 setLiked: (templateId: string, liked: boolean) => void // 更新单个收藏状态 setFavorited: (templateId: string, favorited: boolean) => void - // 获取点赞状态 + // 更新单个点赞数量 + setLikeCount: (templateId: string, count: number) => void + + // 增加点赞数量(乐观更新) + incrementLikeCount: (templateId: string) => void + + // 减少点赞数量(乐观更新) + decrementLikeCount: (templateId: string) => void + + // 获取点赞状态(如果不存在返回 undefined) + getLiked: (templateId: string) => boolean | undefined + + // 获取收藏状态(如果不存在返回 undefined) + getFavorited: (templateId: string) => boolean | undefined + + // 获取点赞数量(如果不存在返回 undefined) + getLikeCount: (templateId: string) => number | undefined + + // 获取点赞状态(如果不存在返回 false) isLiked: (templateId: string) => boolean - // 获取收藏状态 + // 获取收藏状态(如果不存在返回 false) isFavorited: (templateId: string) => boolean // 清空所有缓存 clear: () => void } -export const useTemplateSocialStore = create((set, get) => ({ - // State - likedMap: {}, - favoritedMap: {}, - - // Actions - setLikedStates: (states) => { - set((state) => ({ - likedMap: { ...state.likedMap, ...states }, - })) - }, - - setFavoritedStates: (states) => { - set((state) => ({ - favoritedMap: { ...state.favoritedMap, ...states }, - })) - }, - - setLiked: (templateId, liked) => { - set((state) => ({ - likedMap: { ...state.likedMap, [templateId]: liked }, - })) - }, - - setFavorited: (templateId, favorited) => { - set((state) => ({ - favoritedMap: { ...state.favoritedMap, [templateId]: favorited }, - })) - }, - - isLiked: (templateId) => { - return get().likedMap[templateId] || false - }, - - isFavorited: (templateId) => { - return get().favoritedMap[templateId] || false - }, - - clear: () => { - set({ +export const useTemplateSocialStore = create()( + devtools( + (set, get) => ({ + // State likedMap: {}, favoritedMap: {}, - }) - }, -})) + likeCountMap: {}, + + // Actions + setLikedStates: (states) => { + set((state) => ({ + likedMap: { ...state.likedMap, ...states }, + })) + }, + + setFavoritedStates: (states) => { + set((state) => ({ + favoritedMap: { ...state.favoritedMap, ...states }, + })) + }, + + setLikeCountStates: (states) => { + set((state) => ({ + likeCountMap: { ...state.likeCountMap, ...states }, + })) + }, + + setLiked: (templateId, liked) => { + set((state) => ({ + likedMap: { ...state.likedMap, [templateId]: liked }, + })) + }, + + setFavorited: (templateId, favorited) => { + set((state) => ({ + favoritedMap: { ...state.favoritedMap, [templateId]: favorited }, + })) + }, + + setLikeCount: (templateId, count) => { + set((state) => ({ + likeCountMap: { ...state.likeCountMap, [templateId]: count }, + })) + }, + + incrementLikeCount: (templateId) => { + set((state) => ({ + likeCountMap: { + ...state.likeCountMap, + [templateId]: (state.likeCountMap[templateId] || 0) + 1, + }, + })) + }, + + decrementLikeCount: (templateId) => { + set((state) => { + const currentCount = state.likeCountMap[templateId] || 0 + return { + likeCountMap: { + ...state.likeCountMap, + [templateId]: Math.max(0, currentCount - 1), + }, + } + }) + }, + + getLiked: (templateId) => { + return get().likedMap[templateId] + }, + + getFavorited: (templateId) => { + return get().favoritedMap[templateId] + }, + + getLikeCount: (templateId) => { + return get().likeCountMap[templateId] + }, + + isLiked: (templateId) => { + return get().likedMap[templateId] || false + }, + + isFavorited: (templateId) => { + return get().favoritedMap[templateId] || false + }, + + clear: () => { + set({ + likedMap: {}, + favoritedMap: {}, + likeCountMap: {}, + }) + }, + }), + { name: 'TemplateSocialStore' } + ) +) + +// 选择器 hook:只订阅特定模板的状态 +// 当该模板的状态变化时才会触发重新渲染 +export function useTemplateLiked(templateId: string | undefined) { + return useTemplateSocialStore((state) => + templateId ? state.likedMap[templateId] : undefined + ) +} + +export function useTemplateFavorited(templateId: string | undefined) { + return useTemplateSocialStore((state) => + templateId ? state.favoritedMap[templateId] : undefined + ) +} + +export function useTemplateLikeCount(templateId: string | undefined) { + return useTemplateSocialStore((state) => + templateId ? state.likeCountMap[templateId] : undefined + ) +}