refactor(PostCard, PostCardGrid, HomeScreen): enhance PostCard component and remove PostCardGrid
- Introduced a new PostCardRelativeTime component for dynamic time display, improving user experience with real-time updates. - Refactored PostCard to utilize memoization for performance optimization and prevent unnecessary re-renders. - Removed the PostCardGrid component to streamline the codebase, consolidating functionality within the PostCard component. - Updated HomeScreen to leverage the new PostCard features, ensuring consistent post rendering and interaction handling.
This commit is contained in:
@@ -12,8 +12,8 @@
|
||||
* />
|
||||
*/
|
||||
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { View, TouchableOpacity, StyleSheet, Alert } from 'react-native';
|
||||
import React, { useMemo, useState, memo, useEffect } from 'react';
|
||||
import { View, TouchableOpacity, StyleSheet, Alert, TextStyle } from 'react-native';
|
||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { PostCardProps, PostCardAction } from './types';
|
||||
import { ImageGridItem, SmartImage } from '../../common';
|
||||
@@ -23,12 +23,95 @@ import { colors, spacing, borderRadius, fontSizes } from '../../../theme';
|
||||
import { getPreviewImageUrl } from '../../../utils/imageHelper';
|
||||
import PostImages from './components/PostImages';
|
||||
import { useResponsive } from '../../../hooks/useResponsive';
|
||||
import { formatPostCreatedAtString } from '../../../core/entities/Post';
|
||||
|
||||
/** 列表相对时间:独立定时刷新,避免外层 PostCard.memo 挡住「分钟前」更新 */
|
||||
const PostCardRelativeTime: React.FC<{ createdAt?: string | null; style?: TextStyle | TextStyle[] }> = ({
|
||||
createdAt,
|
||||
style: timeStyle,
|
||||
}) => {
|
||||
const [label, setLabel] = useState(() => formatPostCreatedAtString(createdAt));
|
||||
|
||||
useEffect(() => {
|
||||
setLabel(formatPostCreatedAtString(createdAt));
|
||||
const tick = () => setLabel(formatPostCreatedAtString(createdAt));
|
||||
const id = setInterval(tick, 60_000);
|
||||
return () => clearInterval(id);
|
||||
}, [createdAt]);
|
||||
|
||||
return (
|
||||
<Text style={timeStyle} numberOfLines={1}>
|
||||
{label}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
function imagesSignature(
|
||||
images: PostCardProps['post']['images'] | undefined
|
||||
): string {
|
||||
if (!images?.length) return '';
|
||||
return images.map((i) => i.id).join('\u001f');
|
||||
}
|
||||
|
||||
function authorSignature(author: PostCardProps['post']['author']): string {
|
||||
if (!author) return '';
|
||||
return [author.id, author.nickname ?? '', author.avatar ?? ''].join('\u001f');
|
||||
}
|
||||
|
||||
function topCommentSignature(tc: PostCardProps['post']['top_comment']): string {
|
||||
if (!tc) return '';
|
||||
return [tc.id, tc.content ?? '', tc.author?.id ?? ''].join('\u001f');
|
||||
}
|
||||
|
||||
function featuresComparable(f: PostCardProps['features']): string {
|
||||
if (f == null) return '';
|
||||
if (typeof f === 'string') return f;
|
||||
return JSON.stringify(f);
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义比较:忽略 onAction 引用(父组件应用 postId + ref 解析最新 post,避免 Tab 切换时全列表因回调重建而重绘)。
|
||||
* index 未参与 UI,不比较以免列表重排时多余更新。
|
||||
*/
|
||||
function arePostCardPropsEqual(prev: PostCardProps, next: PostCardProps): boolean {
|
||||
if (prev.variant !== next.variant) return false;
|
||||
if (prev.isPostAuthor !== next.isPostAuthor) return false;
|
||||
if (prev.isAuthor !== next.isAuthor) return false;
|
||||
if (prev.style !== next.style) return false;
|
||||
if (featuresComparable(prev.features) !== featuresComparable(next.features)) return false;
|
||||
|
||||
const a = prev.post;
|
||||
const b = next.post;
|
||||
if (a === b) return true;
|
||||
|
||||
if (a.id !== b.id) return false;
|
||||
if (a.title !== b.title) return false;
|
||||
if (a.content !== b.content) return false;
|
||||
if ((a.likes_count ?? 0) !== (b.likes_count ?? 0)) return false;
|
||||
if ((a.comments_count ?? 0) !== (b.comments_count ?? 0)) return false;
|
||||
if ((a.favorites_count ?? 0) !== (b.favorites_count ?? 0)) return false;
|
||||
if ((a.shares_count ?? 0) !== (b.shares_count ?? 0)) return false;
|
||||
if ((a.views_count ?? 0) !== (b.views_count ?? 0)) return false;
|
||||
if (!!a.is_pinned !== !!b.is_pinned) return false;
|
||||
if (!!a.is_locked !== !!b.is_locked) return false;
|
||||
if (!!a.is_vote !== !!b.is_vote) return false;
|
||||
if (a.created_at !== b.created_at) return false;
|
||||
if (a.updated_at !== b.updated_at) return false;
|
||||
if (a.content_edited_at !== b.content_edited_at) return false;
|
||||
if (!!a.is_liked !== !!b.is_liked) return false;
|
||||
if (!!a.is_favorited !== !!b.is_favorited) return false;
|
||||
if (authorSignature(a.author) !== authorSignature(b.author)) return false;
|
||||
if (imagesSignature(a.images) !== imagesSignature(b.images)) return false;
|
||||
if (topCommentSignature(a.top_comment) !== topCommentSignature(b.top_comment)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* PostCard 主组件
|
||||
* 仅支持新 API
|
||||
*/
|
||||
const PostCard: React.FC<PostCardProps> = (normalizedProps) => {
|
||||
const PostCardInner: React.FC<PostCardProps> = (normalizedProps) => {
|
||||
const {
|
||||
post,
|
||||
onAction,
|
||||
@@ -247,9 +330,7 @@ const PostCard: React.FC<PostCardProps> = (normalizedProps) => {
|
||||
<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>
|
||||
<PostCardRelativeTime createdAt={post.created_at} style={styles.timeText} />
|
||||
{post.is_pinned && (
|
||||
<View style={styles.pinnedTag}>
|
||||
<MaterialCommunityIcons name="pin" size={10} color={colors.warning.main} />
|
||||
@@ -334,6 +415,10 @@ const PostCard: React.FC<PostCardProps> = (normalizedProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
PostCardInner.displayName = 'PostCard';
|
||||
|
||||
const PostCard = memo(PostCardInner, arePostCardPropsEqual);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
backgroundColor: colors.background.paper,
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
/**
|
||||
* PostCardGrid 容器组件
|
||||
* 网格模式(小红书风格)的帖子卡片容器
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { TouchableOpacity, StyleSheet, StyleProp, ViewStyle, GestureResponderEvent } from 'react-native';
|
||||
import { colors, borderRadius } from '../../../theme';
|
||||
import { useResponsive } from '../../../hooks/useResponsive';
|
||||
import { PostCardGridProps } from './types';
|
||||
import { usePostCardActions } from './hooks/usePostCardActions';
|
||||
import { usePostGridStyles } from './hooks/usePostCardStyles';
|
||||
import { PostGridCover, PostGridInfo } from './components';
|
||||
import { convertToImageGridItems, getConsistentAspectRatio } from './utils';
|
||||
|
||||
const PostCardGrid: React.FC<PostCardGridProps> = ({
|
||||
post,
|
||||
onAction,
|
||||
features,
|
||||
style,
|
||||
}) => {
|
||||
const { isDesktop } = useResponsive();
|
||||
const gridStyles = usePostGridStyles();
|
||||
|
||||
const {
|
||||
handlePress,
|
||||
handleUserPress,
|
||||
handleImagePress,
|
||||
} = usePostCardActions(onAction);
|
||||
|
||||
// 防御性检查:确保 author 存在
|
||||
const author = post.author ?? {
|
||||
id: '',
|
||||
nickname: '匿名用户',
|
||||
avatar: '',
|
||||
username: '',
|
||||
cover_url: '',
|
||||
bio: '',
|
||||
website: '',
|
||||
location: '',
|
||||
posts_count: 0,
|
||||
followers_count: 0,
|
||||
following_count: 0,
|
||||
created_at: '',
|
||||
};
|
||||
|
||||
// 获取封面图(第一张图)
|
||||
const coverImage = post.images && post.images.length > 0 ? post.images[0] : null;
|
||||
|
||||
// 根据帖子 ID 生成一致的宽高比
|
||||
const aspectRatio = getConsistentAspectRatio(post.id);
|
||||
|
||||
// 样式计算
|
||||
const containerStyle: StyleProp<ViewStyle> = [
|
||||
styles.container,
|
||||
!coverImage && styles.containerNoImage,
|
||||
isDesktop && styles.containerDesktop,
|
||||
];
|
||||
|
||||
const handleContainerPress = () => {
|
||||
handlePress();
|
||||
};
|
||||
|
||||
const handleImagePressLocal = () => {
|
||||
if (post.images && post.images.length > 0) {
|
||||
handleImagePress(convertToImageGridItems(post.images), 0);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUserPressLocal = () => {
|
||||
handleUserPress();
|
||||
};
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[containerStyle, style]}
|
||||
onPress={handleContainerPress}
|
||||
activeOpacity={0.9}
|
||||
>
|
||||
{/* 封面 */}
|
||||
<PostGridCover
|
||||
image={coverImage}
|
||||
content={post.content}
|
||||
isVote={post.is_vote}
|
||||
aspectRatio={aspectRatio}
|
||||
onImagePress={handleImagePressLocal}
|
||||
/>
|
||||
|
||||
{/* 标题和信息 */}
|
||||
<PostGridInfo
|
||||
title={post.title}
|
||||
author={author}
|
||||
likesCount={post.likes_count}
|
||||
onUserPress={handleUserPressLocal}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
backgroundColor: '#FFF',
|
||||
overflow: 'hidden',
|
||||
borderRadius: 8,
|
||||
},
|
||||
containerDesktop: {
|
||||
borderRadius: 12,
|
||||
},
|
||||
containerNoImage: {
|
||||
minHeight: 200,
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
});
|
||||
|
||||
export default PostCardGrid;
|
||||
@@ -1,12 +1,5 @@
|
||||
/**
|
||||
* PostCard 子组件导出
|
||||
* PostCard 子组件导出(仅导出仓库内已存在的实现)
|
||||
*/
|
||||
|
||||
export { default as PostHeader } from './PostHeader';
|
||||
export { default as PostTitle } from './PostTitle';
|
||||
export { default as PostContent } from './PostContent';
|
||||
export { default as PostImages } from './PostImages';
|
||||
export { default as PostActions } from './PostActions';
|
||||
export { default as PostTopComment } from './PostTopComment';
|
||||
export { default as PostGridCover } from './PostGridCover';
|
||||
export { default as PostGridInfo } from './PostGridInfo';
|
||||
@@ -1,8 +1,5 @@
|
||||
/**
|
||||
* PostCard Hooks 导出
|
||||
* PostCard Hooks(预留入口;实现文件尚未加入仓库时勿导出,避免 TS 无法解析)
|
||||
*/
|
||||
|
||||
export { usePostCardActions } from './usePostCardActions';
|
||||
export { usePostCardFeatures } from './usePostCardFeatures';
|
||||
export { usePostCardStyles, usePostGridStyles } from './usePostCardStyles';
|
||||
export type { PostCardStylesConfig, PostGridStylesConfig } from './usePostCardStyles';
|
||||
export {};
|
||||
|
||||
Reference in New Issue
Block a user