import React, { memo, useCallback, useMemo } from 'react' import { Pressable, StyleSheet, Text, View, ViewStyle, ImageStyle } from 'react-native' import { Image } from 'expo-image' import { LinearGradient } from 'expo-linear-gradient' import { Ionicons } from '@expo/vector-icons' import { useTranslation } from 'react-i18next' import { useTemplateSocialStore, useTemplateLiked, useTemplateFavorited, useTemplateLikeCount } from '@/stores/templateSocialStore' export interface TemplateCardProps { id?: string title: string titleEn?: string previewUrl?: string webpPreviewUrl?: string coverImageUrl?: string aspectRatio?: string cardWidth: number onPress: (id: string) => void liked?: boolean favorited?: boolean likeCount?: number onLike?: (id: string) => void onUnlike?: (id: string) => void onFavorite?: (id: string) => void onUnfavorite?: (id: string) => void testID?: string } /** * 解析 aspectRatio 字符串为数字 * 例如: "128:128" -> 1, "16:9" -> 1.777..., "1.5" -> 1.5 */ export function parseAspectRatio(aspectRatioString?: string): number | undefined { if (!aspectRatioString) return undefined if (aspectRatioString.includes(':')) { const [w, h] = aspectRatioString.split(':').map(Number) return w / h } return Number(aspectRatioString) } /** * 获取图片源 URI,按优先级: webpPreviewUrl > previewUrl > coverImageUrl */ export function getImageUri( webpPreviewUrl?: string, previewUrl?: string, coverImageUrl?: string ): string | undefined { 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 } const GRADIENT_END = { x: 0, y: 1 } const TemplateCardComponent: React.FC = ({ id, title, titleEn, previewUrl, webpPreviewUrl, coverImageUrl, aspectRatio: aspectRatioString, cardWidth, onPress, liked: likedProp, favorited: favoritedProp, likeCount: likeCountProp, onLike, onUnlike, onFavorite, onUnfavorite, testID, }) => { const { i18n } = useTranslation() // 订阅特定模板的点赞/收藏状态和点赞数量 // 当该模板的状态变化时才会触发重新渲染,不影响其他卡片 const storeLiked = useTemplateLiked(id) const storeFavorited = useTemplateFavorited(id) const storeLikeCount = useTemplateLikeCount(id) // 合并 props 状态和 store 状态:store 优先(乐观更新) 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]) // Select display title based on current language const displayTitle = useMemo(() => { const isEnglish = i18n.language === 'en-US' return isEnglish && titleEn ? titleEn : title }, [i18n.language, title, titleEn]) // 计算图标状态 const likeIconName = useMemo(() => (liked ? 'heart' : 'heart-outline'), [liked]) const likeIconColor = useMemo(() => (liked ? '#FF3B30' : 'rgba(142, 142, 147, 0.7)'), [liked]) const favoriteIconName = useMemo(() => (favorited ? 'star' : 'star-outline'), [favorited]) const favoriteIconColor = useMemo(() => (favorited ? '#FFD700' : 'rgba(142, 142, 147, 0.7)'), [favorited]) // 使用 useCallback 缓存 onPress 回调 const handlePress = useCallback(() => { if (id) { onPress(id) } }, [id, onPress]) // 点赞按钮点击处理 - 阻止事件冒泡 const handleLikePress = useCallback(() => { if (id) { if (liked && onUnlike) { onUnlike(id) } else if (!liked && onLike) { onLike(id) } } }, [id, liked, onLike, onUnlike]) // 收藏按钮点击处理 - 阻止事件冒泡 const handleFavoritePress = useCallback(() => { if (id) { if (favorited && onUnfavorite) { onUnfavorite(id) } else if (!favorited && onFavorite) { onFavorite(id) } } }, [id, favorited, onFavorite, onUnfavorite]) // 缓存样式计算 const cardStyle = useMemo(() => [styles.card, { width: cardWidth }], [cardWidth]) const containerStyle = useMemo(() => [styles.cardImageContainer, aspectRatio ? { aspectRatio } : undefined].filter(Boolean) as ViewStyle[], [aspectRatio] ) const imageStyle = useMemo(() => [styles.cardImage, aspectRatio ? { aspectRatio } : undefined].filter(Boolean) as ImageStyle[], [aspectRatio] ) // 如果没有 id,则不渲染卡片 if (!id) { return null } // 判断是否有点赞/收藏回调 const hasSocialActions = !!(onLike || onUnlike || onFavorite || onUnfavorite) return ( {/* 点赞和收藏图标 - 只在有回调时显示为可点击 */} {hasSocialActions ? ( { e.stopPropagation() handleLikePress() }} style={styles.iconButton} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }} testID={`${testID}-like-button`} > {likeCount !== undefined && likeCount > 0 && ( {formatCount(likeCount)} )} { e.stopPropagation() handleFavoritePress() }} style={styles.iconButton} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }} testID={`${testID}-favorite-button`} > ) : ( // 没有回调时显示为静态图标 {likeCount !== undefined && likeCount > 0 && ( {formatCount(likeCount)} )} )} {displayTitle} ) } export const TemplateCard = memo(TemplateCardComponent) const styles = StyleSheet.create({ card: { marginBottom: 5, }, cardImageContainer: { width: '100%', borderRadius: 8, overflow: 'hidden', position: 'relative', }, cardImage: { width: '100%', borderRadius: 8, }, cardImageGradient: { position: 'absolute', bottom: 0, left: 0, right: 0, height: '50%', }, iconsContainer: { position: 'absolute', top: 8, right: 8, flexDirection: 'row', gap: 4, }, likeButtonContainer: { flexDirection: 'row', alignItems: 'center', gap: 2, }, iconButton: { padding: 4, }, icon: { shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.3, 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, left: 8, right: 8, color: '#F5F5F5', fontSize: 12, fontWeight: '500', }, })