refactor(PostCard, PostCard.legacy): remove legacy PostCard component and update exports
- Deleted the legacy PostCard component to streamline the codebase and improve maintainability. - Updated exports in PostCard and index files to remove references to the legacy component. - Adjusted PostCardProps to eliminate legacy properties, ensuring only the new API is supported. - Enhanced the PostCard component to focus on modern implementation and features.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* PostCard 主组件
|
||||
* 帖子卡片组件 - 支持列表模式和网格模式
|
||||
* 帖子卡片组件(新实现)
|
||||
*
|
||||
* @example
|
||||
* // 新 API 使用方式
|
||||
@@ -12,63 +12,543 @@
|
||||
* />
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { PostCardProps, LegacyPostCardProps } from './types';
|
||||
import PostCardList from './PostCardList';
|
||||
import PostCardGrid from './PostCardGrid';
|
||||
import { usePostCardFeatures } from './hooks/usePostCardFeatures';
|
||||
import { createLegacyAdapter, isLegacyProps } from './legacyAdapter';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { View, TouchableOpacity, StyleSheet, Alert } from 'react-native';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { PostCardProps, PostCardAction } from './types';
|
||||
import { ImageGridItem, SmartImage } from '../../common';
|
||||
import Text from '../../common/Text';
|
||||
import Avatar from '../../common/Avatar';
|
||||
import { colors, spacing, borderRadius, fontSizes } from '../../../theme';
|
||||
import { getPreviewImageUrl } from '../../../utils/imageHelper';
|
||||
import PostImages from './components/PostImages';
|
||||
import { useResponsive } from '../../../hooks/useResponsive';
|
||||
|
||||
/**
|
||||
* PostCard 主组件
|
||||
* 支持新旧两种 API
|
||||
* 仅支持新 API
|
||||
*/
|
||||
const PostCard: React.FC<PostCardProps | LegacyPostCardProps> = (props) => {
|
||||
// 检测是否使用旧 API
|
||||
const isLegacy = isLegacyProps(props as LegacyPostCardProps);
|
||||
|
||||
// 兼容层:转换旧 Props 到新 Props
|
||||
const normalizedProps: PostCardProps = isLegacy
|
||||
? createLegacyAdapter(props as LegacyPostCardProps)
|
||||
: (props as PostCardProps);
|
||||
|
||||
const PostCard: React.FC<PostCardProps> = (normalizedProps) => {
|
||||
const {
|
||||
post,
|
||||
onAction,
|
||||
variant = 'list',
|
||||
features: featuresConfig,
|
||||
features,
|
||||
isPostAuthor = false,
|
||||
isAuthor = false,
|
||||
index,
|
||||
style,
|
||||
} = normalizedProps;
|
||||
|
||||
// 解析 Features 配置
|
||||
const resolvedFeatures = usePostCardFeatures(featuresConfig);
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
const isCompact =
|
||||
features === 'compact' ||
|
||||
(typeof features === 'object' && features?.showContent === false);
|
||||
|
||||
const emit = (action: PostCardAction) => {
|
||||
onAction(action);
|
||||
};
|
||||
|
||||
const handleImagePress = (images: ImageGridItem[], imageIndex: number) => {
|
||||
emit({ type: 'imagePress', payload: { images, imageIndex } });
|
||||
};
|
||||
|
||||
const showGrid = variant === 'grid';
|
||||
const author = post.author;
|
||||
const rawContent = post.content ?? '';
|
||||
const { isDesktop, isTablet, isWideScreen, isLandscape } = useResponsive();
|
||||
|
||||
const handleCardPress = () => emit({ type: 'press' });
|
||||
const handleUserPress = () => emit({ type: 'userPress' });
|
||||
const handleLike = () => emit({ type: post.is_liked ? 'unlike' : 'like' });
|
||||
const handleComment = () => emit({ type: 'comment' });
|
||||
const handleBookmark = () => emit({ type: post.is_favorited ? 'unbookmark' : 'bookmark' });
|
||||
const handleShare = () => emit({ type: 'share' });
|
||||
const handleDelete = () => {
|
||||
if (isDeleting) return;
|
||||
Alert.alert(
|
||||
'删除帖子',
|
||||
'确定要删除这篇帖子吗?删除后将无法恢复。',
|
||||
[
|
||||
{ text: '取消', style: 'cancel' },
|
||||
{
|
||||
text: '删除',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
emit({ type: 'delete' });
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
const images: ImageGridItem[] = Array.isArray(post.images)
|
||||
? post.images.map((img) => ({
|
||||
id: img.id,
|
||||
url: img.url,
|
||||
thumbnail_url: img.thumbnail_url,
|
||||
preview_url: img.preview_url,
|
||||
preview_url_large: img.preview_url_large,
|
||||
width: img.width,
|
||||
height: img.height,
|
||||
}))
|
||||
: [];
|
||||
|
||||
const formatNumber = (num: number): string => {
|
||||
if (num >= 10000) return `${(num / 10000).toFixed(1)}w`;
|
||||
if (num >= 1000) return `${(num / 1000).toFixed(1)}k`;
|
||||
return String(num);
|
||||
};
|
||||
|
||||
const getResponsiveMaxLength = () => {
|
||||
if (isWideScreen) return 300;
|
||||
if (isDesktop) return 250;
|
||||
if (isTablet) return 200;
|
||||
return 100;
|
||||
};
|
||||
|
||||
const contentNumberOfLines = useMemo(() => {
|
||||
if (isWideScreen) return 8;
|
||||
if (isDesktop) return 6;
|
||||
if (isTablet) return 5;
|
||||
return 3;
|
||||
}, [isWideScreen, isDesktop, isTablet]);
|
||||
|
||||
const getTruncatedContent = (value: string): string => {
|
||||
const maxLength = getResponsiveMaxLength();
|
||||
if (value.length <= maxLength || isExpanded) return value;
|
||||
return `${value.substring(0, maxLength)}...`;
|
||||
};
|
||||
|
||||
const shouldTruncate = !showGrid && !isCompact && rawContent.length > getResponsiveMaxLength();
|
||||
const content = useMemo(
|
||||
() => (showGrid || isCompact ? rawContent : getTruncatedContent(rawContent)),
|
||||
[rawContent, showGrid, isCompact, isExpanded, isWideScreen, isDesktop, isTablet]
|
||||
);
|
||||
|
||||
const renderImages = () => {
|
||||
if (images.length === 0) return null;
|
||||
|
||||
if (showGrid) {
|
||||
const cover = images[0];
|
||||
const coverUrl = getPreviewImageUrl(cover as any, 'grid');
|
||||
return (
|
||||
<TouchableOpacity activeOpacity={0.9} onPress={() => handleImagePress(images, 0)}>
|
||||
<SmartImage
|
||||
source={{ uri: coverUrl || cover.url || '' }}
|
||||
style={styles.gridCover}
|
||||
resizeMode="cover"
|
||||
usePreview={true}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
// 根据 variant 选择渲染模式
|
||||
if (variant === 'grid') {
|
||||
return (
|
||||
<PostCardGrid
|
||||
post={post}
|
||||
onAction={onAction}
|
||||
features={resolvedFeatures}
|
||||
style={style}
|
||||
<PostImages
|
||||
images={post.images || []}
|
||||
displayMode="list"
|
||||
onImagePress={handleImagePress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const renderTopComment = () => {
|
||||
if (showGrid || !post.top_comment) return null;
|
||||
return (
|
||||
<TouchableOpacity style={styles.topCommentBox} activeOpacity={0.85} onPress={handleComment}>
|
||||
<Text style={styles.topCommentAuthor} numberOfLines={1}>
|
||||
{post.top_comment.author?.nickname || '匿名用户'}:
|
||||
</Text>
|
||||
<Text style={styles.topCommentText} numberOfLines={1}>
|
||||
{post.top_comment.content}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
const renderGridCard = () => {
|
||||
const cover = images[0];
|
||||
const hasImage = !!cover;
|
||||
const coverUrl = hasImage ? getPreviewImageUrl(cover as any, 'grid') : '';
|
||||
const contentPreview = rawContent
|
||||
.replace(/[#*_~`\[\]()]/g, '')
|
||||
.replace(/\n+/g, ' ')
|
||||
.trim();
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.9}
|
||||
onPress={handleCardPress}
|
||||
style={[styles.gridRootCard, style]}
|
||||
>
|
||||
{hasImage ? (
|
||||
<TouchableOpacity activeOpacity={0.9} onPress={() => handleImagePress(images, 0)}>
|
||||
<SmartImage
|
||||
source={{ uri: coverUrl || cover.url || '' }}
|
||||
style={styles.gridCover}
|
||||
resizeMode="cover"
|
||||
usePreview={true}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<View style={styles.gridNoImagePreview}>
|
||||
<Text style={styles.gridNoImageText} numberOfLines={6}>
|
||||
{contentPreview}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{post.is_vote && (
|
||||
<View style={styles.gridVoteTag}>
|
||||
<MaterialCommunityIcons name="vote" size={11} color={colors.primary.contrast} />
|
||||
<Text style={styles.gridVoteTagText}>投票</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{!!post.title && (
|
||||
<Text style={styles.gridTitleMain} numberOfLines={hasImage ? 2 : 3}>
|
||||
{post.title}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<View style={styles.gridFooter}>
|
||||
<TouchableOpacity onPress={handleUserPress} style={styles.gridUserArea}>
|
||||
<Avatar source={author?.avatar} size={20} name={author?.nickname || '匿名用户'} />
|
||||
<Text style={styles.gridUsername} numberOfLines={1}>{author?.nickname || '匿名用户'}</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.gridLikeArea}>
|
||||
<MaterialCommunityIcons name="heart-outline" size={14} color={colors.text.secondary} />
|
||||
<Text style={styles.gridLikeCount}>{formatNumber(post.likes_count || 0)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<PostCardList
|
||||
post={post}
|
||||
onAction={onAction}
|
||||
features={resolvedFeatures}
|
||||
isPostAuthor={isPostAuthor}
|
||||
isAuthor={isAuthor}
|
||||
index={index}
|
||||
style={style}
|
||||
/>
|
||||
showGrid ? renderGridCard() : (
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.9}
|
||||
onPress={handleCardPress}
|
||||
style={[styles.card, style]}
|
||||
>
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity onPress={handleUserPress} activeOpacity={0.8} style={styles.authorWrap}>
|
||||
<Avatar source={author?.avatar} size={showGrid ? 22 : 36} name={author?.nickname || '匿名用户'} />
|
||||
<View style={styles.authorMeta}>
|
||||
<Text style={styles.authorName} numberOfLines={1}>{author?.nickname || '匿名用户'}</Text>
|
||||
{!showGrid && (
|
||||
<View style={styles.metaRow}>
|
||||
<Text style={styles.timeText} numberOfLines={1}>
|
||||
{post.created_at ? new Date(post.created_at).toLocaleDateString() : ''}
|
||||
</Text>
|
||||
{post.is_pinned && (
|
||||
<View style={styles.pinnedTag}>
|
||||
<MaterialCommunityIcons name="pin" size={10} color={colors.warning.main} />
|
||||
<Text style={styles.pinnedText}>置顶</Text>
|
||||
</View>
|
||||
)}
|
||||
{isAuthor && <Text style={styles.badgeText}>楼主</Text>}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
{isPostAuthor && (
|
||||
<TouchableOpacity onPress={handleDelete} hitSlop={8} style={styles.deleteButton} disabled={isDeleting}>
|
||||
<MaterialCommunityIcons
|
||||
name={isDeleting ? 'loading' : 'delete-outline'}
|
||||
size={isDesktop ? 20 : 18}
|
||||
color={colors.text.hint}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{!!post.title && (
|
||||
<Text style={styles.title} numberOfLines={isCompact ? 2 : undefined}>
|
||||
{post.title}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{!isCompact && !!content && (
|
||||
<>
|
||||
<Text
|
||||
style={styles.content}
|
||||
numberOfLines={isExpanded ? undefined : contentNumberOfLines}
|
||||
>
|
||||
{content}
|
||||
</Text>
|
||||
{shouldTruncate && (
|
||||
<TouchableOpacity onPress={() => setIsExpanded((prev) => !prev)} style={styles.expandBtn}>
|
||||
<Text style={styles.expandText}>{isExpanded ? '收起' : '展开全文'}</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{renderImages()}
|
||||
{renderTopComment()}
|
||||
|
||||
<View style={styles.actions}>
|
||||
<View style={styles.viewsWrap}>
|
||||
<MaterialCommunityIcons name="eye-outline" size={16} color={colors.text.hint} />
|
||||
<Text style={styles.viewsText}>{post.views_count || 0}</Text>
|
||||
</View>
|
||||
<View style={styles.actionButtons}>
|
||||
<TouchableOpacity style={styles.actionBtn} onPress={handleLike}>
|
||||
<MaterialCommunityIcons
|
||||
name={post.is_liked ? 'heart' : 'heart-outline'}
|
||||
size={18}
|
||||
color={post.is_liked ? colors.error.main : colors.text.secondary}
|
||||
/>
|
||||
<Text style={post.is_liked ? styles.activeActionTextMerged : styles.actionText}>
|
||||
{post.likes_count > 0 ? formatNumber(post.likes_count) : '赞'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.actionBtn} onPress={handleComment}>
|
||||
<MaterialCommunityIcons name="comment-outline" size={18} color={colors.text.secondary} />
|
||||
<Text style={styles.actionText}>{post.comments_count > 0 ? formatNumber(post.comments_count) : '评论'}</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.actionBtn} onPress={handleShare}>
|
||||
<MaterialCommunityIcons name="share-outline" size={18} color={colors.text.secondary} />
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.actionBtn} onPress={handleBookmark}>
|
||||
<MaterialCommunityIcons
|
||||
name={post.is_favorited ? 'bookmark' : 'bookmark-outline'}
|
||||
size={18}
|
||||
color={post.is_favorited ? colors.warning.main : colors.text.secondary}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
backgroundColor: colors.background.paper,
|
||||
borderRadius: borderRadius.lg,
|
||||
padding: spacing.md,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: colors.divider,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
authorWrap: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
},
|
||||
authorMeta: {
|
||||
marginLeft: spacing.sm,
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
},
|
||||
authorName: {
|
||||
color: colors.text.primary,
|
||||
fontWeight: '600',
|
||||
fontSize: fontSizes.md,
|
||||
},
|
||||
timeText: {
|
||||
color: colors.text.hint,
|
||||
fontSize: fontSizes.xs,
|
||||
marginTop: 2,
|
||||
},
|
||||
metaRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
marginTop: 2,
|
||||
},
|
||||
pinnedTag: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: `${colors.warning.light}22`,
|
||||
borderRadius: borderRadius.sm,
|
||||
paddingHorizontal: 4,
|
||||
paddingVertical: 1,
|
||||
gap: 2,
|
||||
},
|
||||
pinnedText: {
|
||||
color: colors.warning.main,
|
||||
fontSize: fontSizes.xs,
|
||||
},
|
||||
badgeText: {
|
||||
color: colors.primary.main,
|
||||
fontSize: fontSizes.xs,
|
||||
fontWeight: '600',
|
||||
},
|
||||
deleteButton: {
|
||||
padding: spacing.xs,
|
||||
marginLeft: spacing.sm,
|
||||
},
|
||||
title: {
|
||||
color: colors.text.primary,
|
||||
fontSize: fontSizes.lg,
|
||||
fontWeight: '600',
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
content: {
|
||||
color: colors.text.secondary,
|
||||
fontSize: fontSizes.md,
|
||||
marginBottom: spacing.xs,
|
||||
lineHeight: fontSizes.md * 1.45,
|
||||
},
|
||||
expandBtn: {
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
expandText: {
|
||||
color: colors.primary.main,
|
||||
fontSize: fontSizes.sm,
|
||||
},
|
||||
topCommentBox: {
|
||||
backgroundColor: colors.background.default,
|
||||
borderRadius: borderRadius.sm,
|
||||
paddingHorizontal: spacing.sm,
|
||||
paddingVertical: spacing.xs,
|
||||
marginBottom: spacing.sm,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
topCommentAuthor: {
|
||||
color: colors.text.secondary,
|
||||
fontSize: fontSizes.sm,
|
||||
fontWeight: '600',
|
||||
marginRight: 4,
|
||||
},
|
||||
topCommentText: {
|
||||
color: colors.text.secondary,
|
||||
fontSize: fontSizes.sm,
|
||||
flex: 1,
|
||||
},
|
||||
gridCover: {
|
||||
width: '100%',
|
||||
aspectRatio: 0.78,
|
||||
backgroundColor: colors.background.default,
|
||||
},
|
||||
actions: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
viewsWrap: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
viewsText: {
|
||||
color: colors.text.hint,
|
||||
fontSize: fontSizes.sm,
|
||||
marginLeft: 4,
|
||||
},
|
||||
actionButtons: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
actionBtn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginLeft: spacing.md,
|
||||
},
|
||||
actionText: {
|
||||
color: colors.text.secondary,
|
||||
fontSize: fontSizes.sm,
|
||||
marginLeft: 4,
|
||||
},
|
||||
activeActionText: {
|
||||
color: colors.error.main,
|
||||
},
|
||||
activeActionTextMerged: {
|
||||
color: colors.error.main,
|
||||
fontSize: fontSizes.sm,
|
||||
marginLeft: 4,
|
||||
},
|
||||
gridRootCard: {
|
||||
backgroundColor: colors.background.paper,
|
||||
borderRadius: borderRadius.lg,
|
||||
overflow: 'hidden',
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: colors.divider,
|
||||
},
|
||||
gridNoImagePreview: {
|
||||
backgroundColor: colors.background.default,
|
||||
margin: 6,
|
||||
borderRadius: borderRadius.md,
|
||||
minHeight: 120,
|
||||
padding: 8,
|
||||
},
|
||||
gridNoImageText: {
|
||||
color: colors.text.secondary,
|
||||
fontSize: fontSizes.sm,
|
||||
lineHeight: 20,
|
||||
},
|
||||
gridVoteTag: {
|
||||
position: 'absolute',
|
||||
top: 8,
|
||||
right: 8,
|
||||
backgroundColor: `${colors.primary.main}CC`,
|
||||
borderRadius: borderRadius.full,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
},
|
||||
gridVoteTagText: {
|
||||
color: colors.primary.contrast,
|
||||
fontSize: 10,
|
||||
fontWeight: '600',
|
||||
},
|
||||
gridTitleMain: {
|
||||
color: colors.text.primary,
|
||||
fontSize: fontSizes.md,
|
||||
lineHeight: 20,
|
||||
paddingHorizontal: 8,
|
||||
paddingTop: 8,
|
||||
minHeight: 44,
|
||||
},
|
||||
gridFooter: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 10,
|
||||
},
|
||||
gridUserArea: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
},
|
||||
gridUsername: {
|
||||
color: colors.text.secondary,
|
||||
fontSize: fontSizes.sm,
|
||||
marginLeft: 6,
|
||||
flex: 1,
|
||||
},
|
||||
gridLikeArea: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
gridLikeCount: {
|
||||
color: colors.text.secondary,
|
||||
fontSize: fontSizes.sm,
|
||||
marginLeft: 4,
|
||||
},
|
||||
});
|
||||
|
||||
export default PostCard;
|
||||
Reference in New Issue
Block a user