refactor(PostCard, PostCardGrid, HomeScreen): enhance PostCard component and remove PostCardGrid
All checks were successful
Frontend CI / build-and-push-web (push) Successful in 2m44s
Frontend CI / ota-android (push) Successful in 11m56s
Frontend CI / build-android-apk (push) Successful in 49m3s

- 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:
lafay
2026-03-24 05:52:24 +08:00
parent f9f4e73747
commit b91e78c921
13 changed files with 623 additions and 547 deletions

View File

@@ -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,

View File

@@ -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;

View File

@@ -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';

View File

@@ -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 {};