/** * CommentItem 评论项组件 - 小红书 / 贴吧式平铺列表 * 支持嵌套回复显示、楼层号、身份标识、删除评论、图片显示 */ import React, { useMemo, useState } from 'react'; import { View, TouchableOpacity, StyleSheet, Alert } from 'react-native'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { formatDistanceToNow } from 'date-fns'; import { zhCN } from 'date-fns/locale'; import { spacing, borderRadius, fontSizes, useAppColors, type AppColors } from '../../theme'; import { Comment, CommentImage } from '../../types'; import Text from '../common/Text'; import Avatar from '../common/Avatar'; import { CompactImageGrid, ImageGridItem } from '../common'; interface CommentItemProps { comment: Comment; onUserPress: () => void; onReply: () => void; onLike: () => void; floorNumber?: number; // 楼层号 isAuthor?: boolean; // 是否是楼主 replyToUser?: string; // 回复给哪位用户 onReplyPress?: (comment: Comment) => void; // 点击评论的评论 allReplies?: Comment[]; // 所有回复列表,用于根据 target_id 查找被回复用户 onLoadMoreReplies?: (commentId: string) => void; // 加载更多回复的回调 isCommentAuthor?: boolean; // 当前用户是否为评论作者 onDelete?: (comment: Comment) => void; // 删除评论的回调 onImagePress?: (images: ImageGridItem[], index: number) => void; // 点击图片查看大图 currentUserId?: string; // 当前用户ID,用于判断子评论作者 } function createCommentItemStyles(colors: AppColors) { return StyleSheet.create({ container: { flexDirection: 'row', paddingTop: spacing.md, paddingBottom: spacing.md, paddingHorizontal: spacing.md, backgroundColor: colors.background.paper, borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: colors.divider, }, content: { flex: 1, marginLeft: spacing.sm, minWidth: 0, }, header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: spacing.xs, gap: spacing.xs, }, userInfo: { flexDirection: 'row', alignItems: 'center', flexWrap: 'wrap', flex: 1, minWidth: 0, }, metaDot: { fontSize: fontSizes.xs, color: colors.text.hint, marginHorizontal: 2, }, username: { fontWeight: '600', fontSize: fontSizes.sm, color: colors.text.primary, marginRight: spacing.xs, }, badge: { paddingHorizontal: 4, paddingVertical: 1, borderRadius: 2, marginRight: spacing.xs, }, smallBadge: { paddingHorizontal: 2, paddingVertical: 0, }, authorBadge: { backgroundColor: colors.primary.main, }, adminBadge: { backgroundColor: colors.error.main, }, badgeText: { color: colors.text.inverse, fontSize: fontSizes.xs, fontWeight: '600', }, smallBadgeText: { color: colors.text.inverse, fontSize: 9, fontWeight: '600', }, timeText: { fontSize: fontSizes.xs, flexShrink: 0, }, floorPlain: { fontSize: fontSizes.xs, flexShrink: 0, }, replyReference: { marginBottom: spacing.xs, }, commentContent: { marginBottom: spacing.xs, }, text: { lineHeight: 22, fontSize: fontSizes.md, color: colors.text.primary, }, actions: { flexDirection: 'row', alignItems: 'center', marginTop: spacing.xs, flexWrap: 'wrap', }, actionButton: { flexDirection: 'row', alignItems: 'center', marginRight: spacing.lg, paddingVertical: 4, paddingRight: spacing.xs, }, actionText: { marginLeft: 4, fontSize: fontSizes.xs, }, subRepliesContainer: { marginTop: spacing.sm, marginLeft: 0, paddingLeft: spacing.sm, paddingVertical: spacing.xs, borderLeftWidth: 2, borderLeftColor: colors.divider, backgroundColor: colors.background.default, }, subReplyItem: { flexDirection: 'row', alignItems: 'flex-start', marginBottom: spacing.md, }, subReplyItemLast: { marginBottom: 0, }, subReplyBody: { fontSize: fontSizes.sm, lineHeight: 20, marginTop: 2, }, subReplyContent: { flex: 1, marginLeft: spacing.xs, }, subReplyHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 2, }, subReplyAuthor: { fontWeight: '600', color: colors.text.primary, marginRight: spacing.xs, }, moreRepliesButton: { marginTop: spacing.xs, paddingVertical: spacing.xs, }, replyToText: { fontSize: fontSizes.xs, }, replyToName: { fontSize: fontSizes.xs, fontWeight: '500', }, subReplyActions: { flexDirection: 'row', alignItems: 'center', marginTop: spacing.xs, }, subActionButton: { flexDirection: 'row', alignItems: 'center', marginRight: spacing.sm, paddingVertical: 2, }, subActionText: { marginLeft: 2, fontSize: fontSizes.xs, }, }); } const CommentItem: React.FC = ({ comment, onUserPress, onReply, onLike, floorNumber, isAuthor = false, replyToUser, onReplyPress, allReplies, onLoadMoreReplies, isCommentAuthor = false, onDelete, onImagePress, currentUserId, }) => { const colors = useAppColors(); const styles = useMemo(() => createCommentItemStyles(colors), [colors]); const [isDeleting, setIsDeleting] = useState(false); // 格式化时间 const formatTime = (dateString: string): string => { try { return formatDistanceToNow(new Date(dateString), { addSuffix: true, locale: zhCN, }); } catch { return ''; } }; // 格式化数字 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 handleImagePress = (index: number) => { if (onImagePress && comment.images && comment.images.length > 0) { const images: ImageGridItem[] = comment.images.map(img => ({ id: img.url, url: img.url, })); onImagePress(images, index); } }; // 渲染评论图片 - 使用 CompactImageGrid const renderCommentImages = () => { if (!comment.images || comment.images.length === 0) { return null; } const gridImages: ImageGridItem[] = comment.images.map(img => ({ id: img.url, url: img.url, })); return ( ); }; // 处理删除评论 const handleDelete = () => { if (!onDelete || isDeleting) return; Alert.alert( '删除评论', '确定要删除这条评论吗?删除后将无法恢复。', [ { text: '取消', style: 'cancel', }, { text: '删除', style: 'destructive', onPress: async () => { setIsDeleting(true); try { await onDelete(comment); } catch (error) { console.error('删除评论失败:', error); Alert.alert('删除失败', '删除评论时发生错误,请稍后重试'); } finally { setIsDeleting(false); } }, }, ], ); }; // 渲染身份标识 const renderBadges = () => { const badges = []; const authorId = comment.author?.id || ''; if (isAuthor) { badges.push( 楼主 ); } if (authorId === '1') { // 管理员 badges.push( 管理员 ); } return badges; }; // 渲染楼层号 const renderFloorNumber = () => { if (!floorNumber) return null; // 获取楼层显示文本 const getFloorText = (floor: number): string => { switch (floor) { case 1: return '沙发'; case 2: return '板凳'; case 3: return '地板'; default: return `${floor}楼`; } }; return ( {getFloorText(floorNumber)} ); }; // 根据 target_id 查找被回复用户的昵称 const getTargetUserNickname = (targetId: string): string => { // 首先在顶级评论中查找 if (comment.id === targetId) { return comment.author?.nickname || '用户'; } // 然后在回复列表中查找 if (allReplies) { const targetComment = allReplies.find(r => r.id === targetId); if (targetComment) { return targetComment.author?.nickname || '用户'; } } // 最后在当前评论的 replies 中查找 if (comment.replies) { const targetComment = comment.replies.find(r => r.id === targetId); if (targetComment) { return targetComment.author?.nickname || '用户'; } } return '用户'; }; // 渲染子评论图片 const renderSubReplyImages = (reply: Comment) => { if (!reply.images || reply.images.length === 0) { return null; } const gridImages: ImageGridItem[] = reply.images.map(img => ({ id: img.url, url: img.url, })); return ( ); }; // 处理子评论删除 const handleSubReplyDelete = (reply: Comment) => { if (!onDelete || isDeleting) return; Alert.alert( '删除回复', '确定要删除这条回复吗?删除后将无法恢复。', [ { text: '取消', style: 'cancel', }, { text: '删除', style: 'destructive', onPress: async () => { setIsDeleting(true); try { await onDelete(reply); } catch (error) { console.error('删除回复失败:', error); Alert.alert('删除失败', '删除回复时发生错误,请稍后重试'); } finally { setIsDeleting(false); } }, }, ], ); }; // 渲染评论的评论(子评论)- 抖音/b站风格:平铺展示 const renderSubReplies = () => { if (!comment.replies || comment.replies.length === 0) { return null; } const commentAuthorId = comment.author?.id || ''; return ( {comment.replies.map((reply, replyIndex) => { const replyAuthorId = reply.author?.id || ''; const isLastReply = replyIndex === comment.replies!.length - 1; // 根据 target_id 获取被回复的用户昵称 const targetId = reply.target_id; const targetNickname = targetId ? getTargetUserNickname(targetId) : null; // 判断当前用户是否为子评论作者 const isSubReplyAuthor = currentUserId === replyAuthorId; return ( onReplyPress?.(reply)} activeOpacity={0.7} > {reply.author?.nickname || '用户'} {replyAuthorId === commentAuthorId && ( 楼主 )} {/* 显示回复引用:aaa 回复 bbb */} {targetNickname && ( <> {' '}回复 {targetNickname} )} {/* 显示回复内容(如果有文字) */} {reply.content ? ( {reply.content} ) : null} {/* 显示回复图片(如果有图片) */} {renderSubReplyImages(reply)} {/* 子评论操作按钮 */} onReplyPress?.(reply)} > 回复 {/* 删除按钮 - 子评论作者可见 */} {isSubReplyAuthor && ( handleSubReplyDelete(reply)} disabled={isDeleting} > {isDeleting ? '删除中' : '删除'} )} ); })} {comment.replies_count > (comment.replies?.length || 0) && ( onLoadMoreReplies?.(comment.id)} > 展开剩余 {comment.replies_count - (comment.replies?.length || 0)} 条回复 )} ); }; return ( {/* 用户头像 - 略大便于点击,与正文左对齐 */} {/* 评论内容 */} {/* 用户信息行 - QQ频道风格 */} {comment.author?.nickname || '用户'} {renderBadges()} · {formatTime(comment.created_at || '')} {renderFloorNumber()} {/* 回复引用 */} {replyToUser && ( 回复 @{replyToUser}: )} {/* 评论文本 - 非气泡样式 */} {comment.content} {/* 评论图片 */} {renderCommentImages()} {/* 操作按钮 - 更紧凑 */} {/* 点赞 */} {comment.likes_count > 0 ? formatNumber(comment.likes_count) : '赞'} {/* 回复 */} 回复 {/* 删除按钮 - 只对评论作者显示 */} {isCommentAuthor && ( {isDeleting ? '删除中' : '删除'} )} {/* 评论的评论(子评论)- 抖音/b站风格:平铺展示不开新层级 */} {renderSubReplies()} ); }; export default CommentItem;