This commit is contained in:
imeepos 2026-01-28 19:05:25 +08:00
parent 7f84a348a7
commit 6fc87d59de
5 changed files with 369 additions and 103 deletions

View File

@ -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<string, number> = {}
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}

View File

@ -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}

View File

@ -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}

View File

@ -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<TemplateCardProps> = ({
onPress,
liked: likedProp,
favorited: favoritedProp,
likeCount: likeCountProp,
onLike,
onUnlike,
onFavorite,
@ -76,12 +95,16 @@ const TemplateCardComponent: React.FC<TemplateCardProps> = ({
}) => {
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<TemplateCardProps> = ({
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
testID={`${testID}-like-button`}
>
<Ionicons name={likeIconName} size={16} color={likeIconColor} style={styles.icon} />
<View style={styles.likeButtonContainer}>
<Ionicons name={likeIconName} size={16} color={likeIconColor} style={styles.icon} />
{likeCount !== undefined && likeCount > 0 && (
<Text style={styles.likeCount}>{formatCount(likeCount)}</Text>
)}
</View>
</Pressable>
<Pressable
onPress={(e) => {
@ -196,7 +224,12 @@ const TemplateCardComponent: React.FC<TemplateCardProps> = ({
) : (
// 没有回调时显示为静态图标
<View style={styles.iconsContainer}>
<Ionicons name={likeIconName} size={16} color={likeIconColor} style={styles.icon} />
<View style={styles.likeButtonContainer}>
<Ionicons name={likeIconName} size={16} color={likeIconColor} style={styles.icon} />
{likeCount !== undefined && likeCount > 0 && (
<Text style={styles.likeCount}>{formatCount(likeCount)}</Text>
)}
</View>
<Ionicons name={favoriteIconName} size={16} color={favoriteIconColor} style={styles.icon} />
</View>
)}
@ -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,

View File

@ -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<string, boolean>
// 点赞数量缓存: templateId -> number
likeCountMap: Record<string, number>
// 批量更新点赞状态
setLikedStates: (states: Record<string, boolean>) => void
// 批量更新收藏状态
setFavoritedStates: (states: Record<string, boolean>) => void
// 批量更新点赞数量
setLikeCountStates: (states: Record<string, number>) => 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<TemplateSocialState>((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<TemplateSocialState>()(
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
)
}