/** * PostCard 帖子卡片组件 - QQ频道风格(响应式版本) * 简洁的帖子展示,无卡片边框 * 支持响应式布局,宽屏下优化显示 */ import React, { useState, useCallback, useMemo } from 'react'; import { View, TouchableOpacity, StyleSheet, Alert, useWindowDimensions, } from 'react-native'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { colors, spacing, fontSizes, borderRadius } from '../../theme'; import { Post } from '../../types'; import Text from '../common/Text'; import Avatar from '../common/Avatar'; import { ImageGrid, ImageGridItem, SmartImage } from '../common'; import VotePreview from './VotePreview'; import { useResponsive, useResponsiveValue } from '../../hooks/useResponsive'; interface PostCardProps { post: Post; onPress: () => void; onUserPress: () => void; onLike: () => void; onComment: () => void; onBookmark: () => void; onShare: () => void; onImagePress?: (images: ImageGridItem[], index: number) => void; onDelete?: () => void; compact?: boolean; index?: number; isAuthor?: boolean; isPostAuthor?: boolean; // 当前用户是否为帖子作者 variant?: 'default' | 'grid'; } const PostCard: React.FC = ({ post, onPress, onUserPress, onLike, onComment, onBookmark, onShare, onImagePress, onDelete, compact = false, index, isAuthor = false, isPostAuthor = false, variant = 'default', }) => { const [isExpanded, setIsExpanded] = useState(false); const [isDeleting, setIsDeleting] = useState(false); // 使用响应式 hook const { width, isMobile, isTablet, isDesktop, isWideScreen, isLandscape, orientation } = useResponsive(); const { width: windowWidth } = useWindowDimensions(); // 响应式字体大小 const responsiveFontSize = useResponsiveValue({ xs: fontSizes.md, sm: fontSizes.md, md: fontSizes.lg, lg: isLandscape ? fontSizes.lg : fontSizes.xl, xl: fontSizes.xl, '2xl': fontSizes.xl, '3xl': fontSizes['2xl'], '4xl': fontSizes['2xl'], }); // 响应式标题字体大小 const responsiveTitleFontSize = useResponsiveValue({ xs: fontSizes.lg, sm: fontSizes.lg, md: fontSizes.xl, lg: isLandscape ? fontSizes.xl : fontSizes['2xl'], xl: fontSizes['2xl'], '2xl': fontSizes['2xl'], '3xl': fontSizes['3xl'], '4xl': fontSizes['3xl'], }); // 响应式头像大小 const avatarSize = useResponsiveValue({ xs: 36, sm: 38, md: 40, lg: 44, xl: 48, '2xl': 48, '3xl': 52, '4xl': 56, }); // 响应式内边距 - 宽屏下增加更多内边距 const responsivePadding = useResponsiveValue({ xs: spacing.md, sm: spacing.md, md: spacing.lg, lg: spacing.xl, xl: spacing.xl * 1.2, '2xl': spacing.xl * 1.5, '3xl': spacing.xl * 2, '4xl': spacing.xl * 2.5, }); // 宽屏下额外的卡片内边距 const cardPadding = useMemo(() => { if (isWideScreen) return spacing.xl; if (isDesktop) return spacing.lg; if (isTablet) return spacing.md; return 0; // 移动端无额外内边距 }, [isWideScreen, isDesktop, isTablet]); const formatDateTime = (dateString?: string | null): string => { if (!dateString) return ''; const date = new Date(dateString); if (Number.isNaN(date.getTime())) return ''; const pad = (num: number) => String(num).padStart(2, '0'); return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`; }; const isPostEdited = (createdAt?: string, updatedAt?: string): boolean => { if (!createdAt || !updatedAt) return false; const created = new Date(createdAt).getTime(); const updated = new Date(updatedAt).getTime(); if (Number.isNaN(created) || Number.isNaN(updated)) return false; return updated - created > 1000; }; const getTruncatedContent = (content: string | undefined | null, maxLength: number = 100): string => { if (!content) return ''; if (content.length <= maxLength || isExpanded) return content; return content.substring(0, maxLength) + '...'; }; // 宽屏下显示更多内容 const getResponsiveMaxLength = () => { if (isWideScreen) return 300; if (isDesktop) return 250; if (isTablet) return 200; return 100; }; const formatNumber = (num: number): string => { if (num >= 10000) { return (num / 10000).toFixed(1) + 'w'; } if (num >= 1000) { return (num / 1000).toFixed(1) + 'k'; } return num.toString(); }; // 处理删除帖子 const handleDelete = () => { if (!onDelete || isDeleting) return; Alert.alert( '删除帖子', '确定要删除这篇帖子吗?删除后将无法恢复。', [ { text: '取消', style: 'cancel', }, { text: '删除', style: 'destructive', onPress: async () => { setIsDeleting(true); try { await onDelete(); } catch (error) { console.error('删除帖子失败:', error); Alert.alert('删除失败', '删除帖子时发生错误,请稍后重试'); } finally { setIsDeleting(false); } }, }, ], ); }; // 渲染图片 - 使用新的 ImageGrid 组件 const renderImages = () => { if (!post.images || !Array.isArray(post.images) || post.images.length === 0) return null; // 转换图片数据格式 const gridImages: ImageGridItem[] = post.images.map(img => ({ id: img.id, url: img.url, thumbnail_url: img.thumbnail_url, width: img.width, height: img.height, })); // 宽屏下显示更大的图片 const imageGap = isDesktop ? 12 : isTablet ? 8 : 4; const imageBorderRadius = isDesktop ? borderRadius.xl : borderRadius.md; return ( ); }; const renderBadges = () => { const badges = []; if (isAuthor) { badges.push( 楼主 ); } if (post.author?.id === '1') { badges.push( 管理员 ); } return badges; }; const renderTopComment = () => { if (!post.top_comment) return null; const { top_comment } = post; const topCommentContainerStyles = [ styles.topCommentContainer, ...(isDesktop ? [styles.topCommentContainerWide] : []) ]; const topCommentAuthorStyles = [ styles.topCommentAuthor, ...(isDesktop ? [styles.topCommentAuthorWide] : []) ]; const topCommentTextStyles = [ styles.topCommentText, ...(isDesktop ? [styles.topCommentTextWide] : []) ]; const moreCommentsStyles = [ styles.moreComments, ...(isDesktop ? [styles.moreCommentsWide] : []) ]; return ( {top_comment.author?.nickname || '匿名用户'}: {top_comment.content} {post.comments_count > 1 && ( 查看全部{formatNumber(post.comments_count)}条评论 )} ); }; // 渲染投票预览 const renderVotePreview = () => { if (!post.is_vote) return null; return ( ); }; // 渲染小红书风格的两栏卡片 const renderGridVariant = () => { // 获取封面图(第一张图) const coverImage = post.images && Array.isArray(post.images) && post.images.length > 0 ? post.images[0] : null; const hasImage = !!coverImage; // 防御性检查:确保 author 存在 const author = post.author || { id: '', nickname: '匿名用户', avatar: null, username: '' }; // 根据图片实际宽高比或使用随机值创建错落感 // 使用帖子 ID 生成一个伪随机值,确保每次渲染一致但不同帖子有差异 const postId = post.id || ''; const hash = postId.split('').reduce((a, b) => a + b.charCodeAt(0), 0); const aspectRatios = [0.7, 0.75, 0.8, 0.85, 0.9, 1, 1.1, 1.2]; const aspectRatio = aspectRatios[hash % aspectRatios.length]; // 获取正文预览(无图时显示) const getContentPreview = (content: string | undefined | null): string => { if (!content) return ''; // 移除 Markdown 标记和多余空白 return content .replace(/[#*_~`\[\]()]/g, '') .replace(/\n+/g, ' ') .trim(); }; const contentPreview = getContentPreview(post.content); // 响应式字体大小(网格模式) const gridTitleFontSize = isDesktop ? 16 : isTablet ? 15 : 14; const gridUsernameFontSize = isDesktop ? 14 : 12; const gridLikeFontSize = isDesktop ? 14 : 12; return ( {/* 封面图 - 只有有图片时才渲染,无图时不显示占位区域 */} {hasImage && ( onImagePress?.(post.images || [], 0)} > )} {/* 无图时的正文预览区域 */} {!hasImage && contentPreview && ( {contentPreview} )} {/* 投票标识 */} {post.is_vote && ( 投票 )} {/* 标题 - 无图时显示更多行 */} {post.title && ( {post.title} )} {/* 底部信息 */} {author.nickname} {formatNumber(post.likes_count)} ); }; // 根据 variant 渲染不同样式 if (variant === 'grid') { return renderGridVariant(); } // 防御性检查:确保 author 存在 const author = post.author || { id: '', nickname: '匿名用户', avatar: null, username: '' }; // 计算响应式内容最大行数 const contentNumberOfLines = useMemo(() => { if (isWideScreen) return 8; if (isDesktop) return 6; if (isTablet) return 5; return 3; }, [isWideScreen, isDesktop, isTablet]); return ( {/* 用户信息 */} {author.nickname} {renderBadges()} 发布 {formatDateTime(post.created_at)} {isPostEdited(post.created_at, post.updated_at) && ( {' · 修改 '}{formatDateTime(post.updated_at)} )} {post.is_pinned && ( 置顶 )} {/* 删除按钮 - 只对帖子作者显示 */} {isPostAuthor && onDelete && ( )} {/* 标题 */} {post.title && ( {post.title} )} {/* 内容 */} {!compact && ( <> {getTruncatedContent(post.content, getResponsiveMaxLength())} {post.content && post.content.length > getResponsiveMaxLength() && ( setIsExpanded(!isExpanded)} style={styles.expandButton} > {isExpanded ? '收起' : '展开全文'} )} )} {/* 图片 */} {renderImages()} {/* 投票预览 */} {renderVotePreview()} {/* 热门评论预览 */} {renderTopComment()} {/* 交互按钮 */} {formatNumber(post.views_count || 0)} {post.likes_count > 0 ? formatNumber(post.likes_count) : '赞'} {post.comments_count > 0 ? formatNumber(post.comments_count) : '评论'} ); }; const styles = StyleSheet.create({ container: { backgroundColor: colors.background.paper, borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: colors.divider, }, // 宽屏卡片样式 wideCard: { backgroundColor: colors.background.paper, borderRadius: borderRadius.lg, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.08, shadowRadius: 8, elevation: 3, }, userSection: { flexDirection: 'row', alignItems: 'center', marginBottom: spacing.sm, }, userInfo: { flex: 1, marginLeft: spacing.sm, }, userNameRow: { flexDirection: 'row', alignItems: 'center', flexWrap: 'wrap', }, username: { fontWeight: '600', color: colors.text.primary, }, badge: { paddingHorizontal: 4, paddingVertical: 1, borderRadius: 2, marginLeft: spacing.xs, }, authorBadge: { backgroundColor: colors.primary.main, }, adminBadge: { backgroundColor: colors.error.main, }, badgeText: { color: colors.text.inverse, fontSize: fontSizes.xs, fontWeight: '600', }, postMeta: { flexDirection: 'row', alignItems: 'center', marginTop: 2, }, timeText: { fontSize: fontSizes.sm, color: colors.text.hint, }, pinnedTag: { flexDirection: 'row', alignItems: 'center', backgroundColor: colors.warning.light + '20', paddingHorizontal: spacing.xs, paddingVertical: 2, borderRadius: borderRadius.sm, }, pinnedText: { marginLeft: 2, fontSize: fontSizes.xs, fontWeight: '500', }, deleteButton: { padding: spacing.xs, marginLeft: spacing.sm, }, title: { marginBottom: spacing.xs, fontWeight: '600', color: colors.text.primary, }, content: { marginBottom: spacing.xs, color: colors.text.secondary, }, expandButton: { marginBottom: spacing.sm, }, imagesContainer: { marginBottom: spacing.md, }, topCommentContainer: { backgroundColor: colors.background.default, borderRadius: borderRadius.sm, padding: spacing.sm, marginBottom: spacing.sm, }, topCommentContainerWide: { padding: spacing.md, borderRadius: borderRadius.md, }, topCommentContent: { flexDirection: 'row', alignItems: 'center', }, topCommentAuthor: { fontWeight: '600', marginRight: spacing.xs, }, topCommentAuthorWide: { fontSize: fontSizes.sm, }, topCommentText: { flex: 1, }, topCommentTextWide: { fontSize: fontSizes.sm, }, moreComments: { marginTop: spacing.xs, fontWeight: '500', }, moreCommentsWide: { fontSize: fontSizes.sm, marginTop: spacing.sm, }, actionBar: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', }, actionBarWide: { marginTop: spacing.sm, paddingTop: spacing.sm, }, viewCount: { flexDirection: 'row', alignItems: 'center', }, actionButtons: { flexDirection: 'row', alignItems: 'center', }, actionButton: { flexDirection: 'row', alignItems: 'center', marginLeft: spacing.md, paddingVertical: spacing.xs, }, actionButtonWide: { marginLeft: spacing.lg, paddingVertical: spacing.sm, }, actionText: { marginLeft: 6, }, actionTextWide: { fontSize: fontSizes.md, }, // ========== 小红书风格两栏样式 ========== gridContainer: { backgroundColor: '#FFF', overflow: 'hidden', borderRadius: 8, }, gridContainerDesktop: { borderRadius: 12, }, gridContainerNoImage: { minHeight: 200, justifyContent: 'space-between', }, gridCoverImage: { width: '100%', aspectRatio: 0.7, backgroundColor: '#F5F5F5', }, gridCoverPlaceholder: { width: '100%', aspectRatio: 0.7, backgroundColor: '#F5F5F5', alignItems: 'center', justifyContent: 'center', }, // 无图时的正文预览区域 gridContentPreview: { backgroundColor: '#F8F8F8', margin: 4, padding: 8, borderRadius: 8, minHeight: 100, }, gridContentPreviewDesktop: { margin: 8, padding: 12, borderRadius: 12, minHeight: 150, }, gridContentText: { fontSize: 13, color: '#666', lineHeight: 20, }, gridContentTextDesktop: { fontSize: 15, lineHeight: 24, }, gridTitle: { color: '#333', lineHeight: 20, paddingHorizontal: 4, paddingTop: 8, minHeight: 44, }, gridTitleNoImage: { minHeight: 0, paddingTop: 4, }, gridTitleDesktop: { paddingHorizontal: 8, paddingTop: 12, lineHeight: 24, minHeight: 56, }, gridFooter: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 4, paddingVertical: 8, }, gridFooterDesktop: { paddingHorizontal: 8, paddingVertical: 12, }, gridUserInfo: { flexDirection: 'row', alignItems: 'center', flex: 1, }, gridUsername: { color: '#666', marginLeft: 6, flex: 1, }, gridLikeInfo: { flexDirection: 'row', alignItems: 'center', }, gridLikeCount: { color: '#666', marginLeft: 4, }, gridVoteBadge: { position: 'absolute', top: 8, right: 8, flexDirection: 'row', alignItems: 'center', backgroundColor: colors.primary.main + 'CC', paddingHorizontal: 8, paddingVertical: 4, borderRadius: borderRadius.full, gap: 4, }, gridVoteBadgeText: { fontSize: 10, color: colors.primary.contrast, fontWeight: '600', }, }); export default PostCard;