Initial frontend repository commit.
Include app source and update .gitignore to exclude local release artifacts and signing files. Made-with: Cursor
This commit is contained in:
911
src/components/business/PostCard.tsx
Normal file
911
src/components/business/PostCard.tsx
Normal file
@@ -0,0 +1,911 @@
|
||||
/**
|
||||
* 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 { formatDistanceToNow } from 'date-fns';
|
||||
import { zhCN } from 'date-fns/locale';
|
||||
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<PostCardProps> = ({
|
||||
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 formatTime = (dateString: string | undefined | null): string => {
|
||||
if (!dateString) return '';
|
||||
try {
|
||||
return formatDistanceToNow(new Date(dateString), {
|
||||
addSuffix: true,
|
||||
locale: zhCN,
|
||||
});
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
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 (
|
||||
<View style={styles.imagesContainer}>
|
||||
<ImageGrid
|
||||
images={gridImages}
|
||||
maxDisplayCount={isWideScreen ? 12 : 9}
|
||||
mode="auto"
|
||||
gap={imageGap}
|
||||
borderRadius={imageBorderRadius}
|
||||
showMoreOverlay={true}
|
||||
onImagePress={onImagePress}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const renderBadges = () => {
|
||||
const badges = [];
|
||||
|
||||
if (isAuthor) {
|
||||
badges.push(
|
||||
<View key="author" style={[styles.badge, styles.authorBadge]}>
|
||||
<Text variant="caption" style={styles.badgeText}>楼主</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (post.author?.id === '1') {
|
||||
badges.push(
|
||||
<View key="admin" style={[styles.badge, styles.adminBadge]}>
|
||||
<Text variant="caption" style={styles.badgeText}>管理员</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<View style={topCommentContainerStyles}>
|
||||
<View style={styles.topCommentContent}>
|
||||
<Text
|
||||
variant="caption"
|
||||
color={colors.text.secondary}
|
||||
style={topCommentAuthorStyles}
|
||||
>
|
||||
{top_comment.author?.nickname || '匿名用户'}:
|
||||
</Text>
|
||||
<Text
|
||||
variant="caption"
|
||||
color={colors.text.secondary}
|
||||
style={topCommentTextStyles}
|
||||
numberOfLines={isDesktop ? 2 : 1}
|
||||
>
|
||||
{top_comment.content}
|
||||
</Text>
|
||||
</View>
|
||||
{post.comments_count > 1 && (
|
||||
<Text
|
||||
variant="caption"
|
||||
color={colors.primary.main}
|
||||
style={moreCommentsStyles}
|
||||
>
|
||||
查看全部{formatNumber(post.comments_count)}条评论
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染投票预览
|
||||
const renderVotePreview = () => {
|
||||
if (!post.is_vote) return null;
|
||||
|
||||
return (
|
||||
<VotePreview
|
||||
onPress={onPress}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染小红书风格的两栏卡片
|
||||
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 (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.gridContainer,
|
||||
!hasImage && styles.gridContainerNoImage,
|
||||
isDesktop && styles.gridContainerDesktop
|
||||
]}
|
||||
onPress={onPress}
|
||||
activeOpacity={0.9}
|
||||
>
|
||||
{/* 封面图 - 只有有图片时才渲染,无图时不显示占位区域 */}
|
||||
{hasImage && (
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.8}
|
||||
onPress={() => onImagePress?.(post.images || [], 0)}
|
||||
>
|
||||
<SmartImage
|
||||
source={{ uri: coverImage.thumbnail_url || coverImage.url || '' }}
|
||||
style={[styles.gridCoverImage, { aspectRatio }]}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
{/* 无图时的正文预览区域 */}
|
||||
{!hasImage && contentPreview && (
|
||||
<View style={[styles.gridContentPreview, isDesktop ? styles.gridContentPreviewDesktop : null]}>
|
||||
<Text
|
||||
style={[styles.gridContentText, ...(isDesktop ? [styles.gridContentTextDesktop] : [])]}
|
||||
numberOfLines={isDesktop ? 8 : 6}
|
||||
>
|
||||
{contentPreview}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 投票标识 */}
|
||||
{post.is_vote && (
|
||||
<View style={styles.gridVoteBadge}>
|
||||
<MaterialCommunityIcons name="vote" size={isDesktop ? 14 : 12} color={colors.primary.contrast} />
|
||||
<Text style={styles.gridVoteBadgeText}>投票</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* 标题 - 无图时显示更多行 */}
|
||||
{post.title && (
|
||||
<Text
|
||||
style={[
|
||||
styles.gridTitle,
|
||||
{ fontSize: gridTitleFontSize },
|
||||
...(hasImage ? [] : [styles.gridTitleNoImage]),
|
||||
...(isDesktop ? [styles.gridTitleDesktop] : [])
|
||||
]}
|
||||
numberOfLines={hasImage ? 2 : 3}
|
||||
>
|
||||
{post.title}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{/* 底部信息 */}
|
||||
<View style={[styles.gridFooter, isDesktop && styles.gridFooterDesktop]}>
|
||||
<TouchableOpacity style={styles.gridUserInfo} onPress={onUserPress}>
|
||||
<Avatar
|
||||
source={author.avatar}
|
||||
size={isDesktop ? 24 : 20}
|
||||
name={author.nickname}
|
||||
/>
|
||||
<Text style={[styles.gridUsername, { fontSize: gridUsernameFontSize }]} numberOfLines={1}>
|
||||
{author.nickname}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={styles.gridLikeInfo}>
|
||||
<MaterialCommunityIcons name="heart" size={isDesktop ? 16 : 14} color="#666" />
|
||||
<Text style={[styles.gridLikeCount, { fontSize: gridLikeFontSize }]}>
|
||||
{formatNumber(post.likes_count)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
// 根据 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 (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.container,
|
||||
{
|
||||
paddingHorizontal: responsivePadding,
|
||||
paddingVertical: responsivePadding * 0.75,
|
||||
...(isDesktop || isWideScreen ? {
|
||||
borderRadius: borderRadius.lg,
|
||||
marginHorizontal: cardPadding,
|
||||
marginVertical: spacing.sm,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.08,
|
||||
shadowRadius: 8,
|
||||
elevation: 3,
|
||||
} : {})
|
||||
}
|
||||
]}
|
||||
onPress={onPress}
|
||||
activeOpacity={0.9}
|
||||
>
|
||||
{/* 用户信息 */}
|
||||
<View style={styles.userSection}>
|
||||
<TouchableOpacity onPress={onUserPress}>
|
||||
<Avatar
|
||||
source={author.avatar}
|
||||
size={avatarSize}
|
||||
name={author.nickname}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.userInfo}>
|
||||
<View style={styles.userNameRow}>
|
||||
<TouchableOpacity onPress={onUserPress}>
|
||||
<Text
|
||||
variant="body"
|
||||
style={[
|
||||
styles.username,
|
||||
{ fontSize: isDesktop ? fontSizes.lg : fontSizes.md }
|
||||
]}
|
||||
>
|
||||
{author.nickname}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
{renderBadges()}
|
||||
</View>
|
||||
<View style={styles.postMeta}>
|
||||
<Text variant="caption" color={colors.text.hint} style={styles.timeText}>
|
||||
{formatTime(post.created_at || '')}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
{post.is_pinned && (
|
||||
<View style={styles.pinnedTag}>
|
||||
<MaterialCommunityIcons name="pin" size={isDesktop ? 14 : 12} color={colors.warning.main} />
|
||||
<Text variant="caption" color={colors.warning.main} style={styles.pinnedText}>置顶</Text>
|
||||
</View>
|
||||
)}
|
||||
{/* 删除按钮 - 只对帖子作者显示 */}
|
||||
{isPostAuthor && onDelete && (
|
||||
<TouchableOpacity
|
||||
style={styles.deleteButton}
|
||||
onPress={handleDelete}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name={isDeleting ? 'loading' : 'delete-outline'}
|
||||
size={isDesktop ? 20 : 18}
|
||||
color={colors.text.hint}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 标题 */}
|
||||
{post.title && (
|
||||
<Text
|
||||
variant="body"
|
||||
numberOfLines={compact ? 2 : undefined}
|
||||
style={[
|
||||
styles.title,
|
||||
{ fontSize: responsiveTitleFontSize, lineHeight: responsiveTitleFontSize * 1.4 }
|
||||
]}
|
||||
>
|
||||
{post.title}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{/* 内容 */}
|
||||
{!compact && (
|
||||
<>
|
||||
<Text
|
||||
variant="body"
|
||||
color={colors.text.secondary}
|
||||
numberOfLines={isExpanded ? undefined : contentNumberOfLines}
|
||||
style={[
|
||||
styles.content,
|
||||
{ fontSize: responsiveFontSize, lineHeight: responsiveFontSize * 1.5 }
|
||||
]}
|
||||
>
|
||||
{getTruncatedContent(post.content, getResponsiveMaxLength())}
|
||||
</Text>
|
||||
{post.content && post.content.length > getResponsiveMaxLength() && (
|
||||
<TouchableOpacity
|
||||
onPress={() => setIsExpanded(!isExpanded)}
|
||||
style={styles.expandButton}
|
||||
>
|
||||
<Text variant="caption" color={colors.primary.main}>
|
||||
{isExpanded ? '收起' : '展开全文'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 图片 */}
|
||||
{renderImages()}
|
||||
|
||||
{/* 投票预览 */}
|
||||
{renderVotePreview()}
|
||||
|
||||
{/* 热门评论预览 */}
|
||||
{renderTopComment()}
|
||||
|
||||
{/* 交互按钮 */}
|
||||
<View style={[styles.actionBar, isDesktop ? styles.actionBarWide : null]}>
|
||||
<View style={styles.viewCount}>
|
||||
<MaterialCommunityIcons name="eye-outline" size={isDesktop ? 18 : 16} color={colors.text.hint} />
|
||||
<Text
|
||||
variant="caption"
|
||||
color={colors.text.hint}
|
||||
style={[styles.actionText, ...(isDesktop ? [styles.actionTextWide] : [])]}
|
||||
>
|
||||
{formatNumber(post.views_count || 0)}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.actionButtons}>
|
||||
<TouchableOpacity style={[styles.actionButton, ...(isDesktop ? [styles.actionButtonWide] : [])]} onPress={onLike}>
|
||||
<MaterialCommunityIcons
|
||||
name={post.is_liked ? 'heart' : 'heart-outline'}
|
||||
size={isDesktop ? 22 : 19}
|
||||
color={post.is_liked ? colors.error.main : colors.text.secondary}
|
||||
/>
|
||||
<Text
|
||||
variant="caption"
|
||||
color={post.is_liked ? colors.error.main : colors.text.secondary}
|
||||
style={[styles.actionText, ...(isDesktop ? [styles.actionTextWide] : [])]}
|
||||
>
|
||||
{post.likes_count > 0 ? formatNumber(post.likes_count) : '赞'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity style={[styles.actionButton, ...(isDesktop ? [styles.actionButtonWide] : [])]} onPress={onComment}>
|
||||
<MaterialCommunityIcons
|
||||
name="comment-outline"
|
||||
size={isDesktop ? 22 : 19}
|
||||
color={colors.text.secondary}
|
||||
/>
|
||||
<Text variant="caption" color={colors.text.secondary} style={[styles.actionText, ...(isDesktop ? [styles.actionTextWide] : [])]}>
|
||||
{post.comments_count > 0 ? formatNumber(post.comments_count) : '评论'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity style={[styles.actionButton, ...(isDesktop ? [styles.actionButtonWide] : [])]} onPress={onShare}>
|
||||
<MaterialCommunityIcons
|
||||
name="share-outline"
|
||||
size={isDesktop ? 22 : 19}
|
||||
color={colors.text.secondary}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity style={[styles.actionButton, isDesktop && styles.actionButtonWide]} onPress={onBookmark}>
|
||||
<MaterialCommunityIcons
|
||||
name={post.is_favorited ? 'bookmark' : 'bookmark-outline'}
|
||||
size={isDesktop ? 22 : 19}
|
||||
color={post.is_favorited ? colors.warning.main : colors.text.secondary}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
Reference in New Issue
Block a user