Switch service integrations and screen/store consumers from websocket events to SSE, and ignore generated dist-web artifacts. Made-with: Cursor
919 lines
25 KiB
TypeScript
919 lines
25 KiB
TypeScript
/**
|
||
* 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<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 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 (
|
||
<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}>
|
||
发布 {formatDateTime(post.created_at)}
|
||
</Text>
|
||
{isPostEdited(post.created_at, post.updated_at) && (
|
||
<Text variant="caption" color={colors.text.hint} style={styles.timeText}>
|
||
{' · 修改 '}{formatDateTime(post.updated_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;
|