Remove unnecessary console logs and replace them with void expressions for better performance and cleaner code. Update error logging to use console.error for consistency. This change enhances code readability and reduces console noise during execution.
Some checks failed
Frontend CI / build-and-push-web (push) Failing after 2m18s
Frontend CI / ota-android (push) Failing after 6m15s
Frontend CI / build-android-apk (push) Failing after 29m51s

This commit is contained in:
2026-03-24 03:02:54 +08:00
parent 5c9cb6acea
commit 82e99d24d8
13 changed files with 728 additions and 32 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "carrot_bbs",
"version": "1.0.0",
"version": "1.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "carrot_bbs",
"version": "1.0.0",
"version": "1.0.1",
"dependencies": {
"@expo/vector-icons": "^15.1.1",
"@react-native-async-storage/async-storage": "^2.2.0",

View File

@@ -0,0 +1,74 @@
/**
* PostCard 主组件
* 帖子卡片组件 - 支持列表模式和网格模式
*
* @example
* // 新 API 使用方式
* <PostCard
* post={post}
* onAction={(action) => handleAction(post, action)}
* variant="list"
* features="full"
* />
*/
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';
/**
* PostCard 主组件
* 支持新旧两种 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 {
post,
onAction,
variant = 'list',
features: featuresConfig,
isPostAuthor = false,
isAuthor = false,
index,
style,
} = normalizedProps;
// 解析 Features 配置
const resolvedFeatures = usePostCardFeatures(featuresConfig);
// 根据 variant 选择渲染模式
if (variant === 'grid') {
return (
<PostCardGrid
post={post}
onAction={onAction}
features={resolvedFeatures}
style={style}
/>
);
}
return (
<PostCardList
post={post}
onAction={onAction}
features={resolvedFeatures}
isPostAuthor={isPostAuthor}
isAuthor={isAuthor}
index={index}
style={style}
/>
);
};
export default PostCard;

View File

@@ -0,0 +1,115 @@
/**
* 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

@@ -0,0 +1,53 @@
/**
* PostImages 子组件
* 显示帖子图片网格
*/
import React from 'react';
import { View, StyleSheet } from 'react-native';
import { colors, spacing, borderRadius } from '../../../../theme';
import { useResponsive } from '../../../../hooks/useResponsive';
import { ImageGrid, ImageGridItem } from '../../../common';
import { PostImagesProps } from '../types';
const PostImages: React.FC<PostImagesProps> = ({ images, displayMode, onImagePress }) => {
const { isDesktop, isWideScreen } = useResponsive();
if (!images || images.length === 0) return null;
// 计算图片间距和圆角
const imageGap = isDesktop ? 12 : 8;
const imageBorderRadius = isDesktop ? borderRadius.xl : borderRadius.md;
return (
<View style={styles.container}>
<ImageGrid
images={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,
}))}
maxDisplayCount={isWideScreen ? 12 : 9}
mode="auto"
gap={imageGap}
borderRadius={imageBorderRadius}
showMoreOverlay={true}
onImagePress={onImagePress}
displayMode={displayMode}
usePreview={true}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
marginBottom: spacing.md,
},
});
export default PostImages;

View File

@@ -0,0 +1,12 @@
/**
* 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

@@ -0,0 +1,8 @@
/**
* PostCard Hooks 导出
*/
export { usePostCardActions } from './usePostCardActions';
export { usePostCardFeatures } from './usePostCardFeatures';
export { usePostCardStyles, usePostGridStyles } from './usePostCardStyles';
export type { PostCardStylesConfig, PostGridStylesConfig } from './usePostCardStyles';

View File

@@ -0,0 +1,58 @@
/**
* PostCard 组件模块导出
*/
// 主组件(默认导出)
export { default } from './PostCard';
// 类型导出
export type {
PostCardActionType,
PostCardActionPayload,
PostCardAction,
PostCardOnAction,
PostCardFeatures,
PostCardFeaturesPreset,
PostCardFeaturesConfig,
PostCardProps,
PostHeaderProps,
PostTitleProps,
PostContentProps,
PostImagesProps,
PostActionsProps,
PostVotePreviewProps,
PostTopCommentProps,
PostGridCoverProps,
PostGridInfoProps,
PostCardListProps,
PostCardGridProps,
LegacyPostCardProps,
Badge,
} from './types';
// 预设配置导出
export { PostCardPresets, defaultFeatures, resolveFeatures } from './presets';
// Hooks 导出
export {
usePostCardActions,
usePostCardFeatures,
usePostCardStyles,
usePostGridStyles,
} from './hooks';
export type { PostCardStylesConfig, PostGridStylesConfig } from './hooks';
// 子组件导出
export {
PostHeader,
PostTitle,
PostContent,
PostImages,
PostActions,
PostTopComment,
PostGridCover,
PostGridInfo,
} from './components';
// 兼容层导出(用于迁移帮助)
export { isLegacyProps, createLegacyAdapter } from './legacyAdapter';

View File

@@ -0,0 +1,285 @@
/**
* PostCard 组件类型定义
* 统一的帖子卡片组件类型系统
*/
import { StyleProp, ViewStyle } from 'react-native';
import { Post, PostImageDTO, UserDTO, CommentDTO } from '../../../types';
import { ImageGridItem } from '../../common';
// ==================== Action 类型定义 ====================
/**
* PostCard 支持的操作类型
*/
export type PostCardActionType =
| 'press' // 点击帖子主体
| 'userPress' // 点击用户头像/名称
| 'like' // 点赞
| 'unlike' // 取消点赞
| 'comment' // 评论
| 'bookmark' // 收藏
| 'unbookmark' // 取消收藏
| 'share' // 分享
| 'imagePress' // 点击图片
| 'delete'; // 删除
/**
* Action payload 类型
*/
export interface PostCardActionPayload {
images?: ImageGridItem[];
imageIndex?: number;
[key: string]: unknown;
}
/**
* 统一的 Action 对象
*/
export interface PostCardAction {
type: PostCardActionType;
payload?: PostCardActionPayload;
}
/**
* 统一的回调函数类型
*/
export type PostCardOnAction = (action: PostCardAction) => void;
// ==================== Features 配置类型 ====================
/**
* 显示特性配置
*/
export interface PostCardFeatures {
/** 显示用户信息 */
showAuthor?: boolean;
/** 显示标题 */
showTitle?: boolean;
/** 显示内容 */
showContent?: boolean;
/** 显示图片 */
showImages?: boolean;
/** 显示投票预览 */
showVote?: boolean;
/** 显示热门评论 */
showTopComment?: boolean;
/** 显示操作栏 */
showActions?: boolean;
/** 显示浏览数 */
showViews?: boolean;
/** 显示分享按钮 */
showShare?: boolean;
/** 显示收藏按钮 */
showBookmark?: boolean;
/** 显示删除按钮(需配合权限判断) */
showDelete?: boolean;
/** 显示置顶标签 */
showPinned?: boolean;
/** 显示徽章(楼主/管理员) */
showBadges?: boolean;
}
/**
* Features 预设名称
*/
export type PostCardFeaturesPreset = 'full' | 'compact' | 'grid';
/**
* Features 配置(可以是预设名称或自定义对象)
*/
export type PostCardFeaturesConfig = PostCardFeaturesPreset | PostCardFeatures;
// ==================== 主组件 Props ====================
/**
* PostCard 主组件 Props
*/
export interface PostCardProps {
/** 帖子数据 */
post: Post;
/** 统一事件回调 */
onAction: PostCardOnAction;
/** 显示模式:列表模式 | 网格模式 */
variant?: 'list' | 'grid';
/** 预设配置名称,或自定义配置对象 */
features?: PostCardFeaturesConfig;
/** 当前用户是否为帖子作者(用于显示删除按钮) */
isPostAuthor?: boolean;
/** 当前用户是否为楼主(在帖子详情页显示"楼主"徽章) */
isAuthor?: boolean;
/** 索引(用于虚拟化列表优化) */
index?: number;
/** 自定义样式 */
style?: StyleProp<ViewStyle>;
}
// ==================== 子组件 Props ====================
/**
* PostHeader 子组件 Props
*/
export interface PostHeaderProps {
author: UserDTO;
createdAt: string;
editedAt?: string;
isPinned?: boolean;
badges?: Array<{ type: 'author' | 'admin'; label: string }>;
onDelete?: () => void;
onUserPress: () => void;
showDelete?: boolean;
showPinned?: boolean;
showBadges?: boolean;
isDeleting?: boolean;
}
/**
* PostTitle 子组件 Props
*/
export interface PostTitleProps {
title?: string;
numberOfLines?: number;
}
/**
* PostContent 子组件 Props
*/
export interface PostContentProps {
content?: string;
compact?: boolean;
maxLines?: number;
isExpanded?: boolean;
onToggleExpand?: () => void;
}
/**
* PostImages 子组件 Props
*/
export interface PostImagesProps {
images: PostImageDTO[];
displayMode: 'list' | 'grid';
onImagePress: (images: ImageGridItem[], index: number) => void;
}
/**
* PostActions 子组件 Props
*/
export interface PostActionsProps {
viewsCount: number;
likesCount: number;
commentsCount: number;
isLiked: boolean;
isFavorited: boolean;
onLike: () => void;
onComment: () => void;
onShare: () => void;
onBookmark: () => void;
showViews?: boolean;
showShare?: boolean;
showBookmark?: boolean;
}
/**
* PostVotePreview 子组件 Props
*/
export interface PostVotePreviewProps {
isVote: boolean;
totalVotes?: number;
optionsCount?: number;
onPress: () => void;
}
/**
* PostTopComment 子组件 Props
*/
export interface PostTopCommentProps {
comment: CommentDTO | null;
totalComments: number;
onPress: () => void;
}
/**
* PostGridCover 子组件 Props
*/
export interface PostGridCoverProps {
image: PostImageDTO | null;
content?: string;
isVote?: boolean;
aspectRatio: number;
onImagePress: () => void;
}
/**
* PostGridInfo 子组件 Props
*/
export interface PostGridInfoProps {
title?: string;
author: UserDTO;
likesCount: number;
onUserPress: () => void;
}
// ==================== 容器组件 Props ====================
/**
* PostCardList 容器组件 Props
*/
export interface PostCardListProps {
post: Post;
onAction: PostCardOnAction;
features: PostCardFeatures;
isPostAuthor?: boolean;
isAuthor?: boolean;
index?: number;
style?: StyleProp<ViewStyle>;
}
/**
* PostCardGrid 容器组件 Props
*/
export interface PostCardGridProps {
post: Post;
onAction: PostCardOnAction;
features: PostCardFeatures;
style?: StyleProp<ViewStyle>;
}
// ==================== 旧版 Props兼容层使用 ====================
/**
* 旧版 PostCard Props用于兼容层
* @deprecated 请迁移到新的 PostCardProps
*/
export interface LegacyPostCardProps {
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';
}
// ==================== 工具类型 ====================
/**
* 徽章类型
*/
export interface Badge {
type: 'author' | 'admin';
label: string;
}

View File

@@ -2,7 +2,21 @@
* 业务组件导出
*/
// PostCard 新架构(从 PostCard 目录导出)
export { default as PostCard } from './PostCard';
// 导出 PostCard 相关类型和工具
export type {
PostCardActionType,
PostCardAction,
PostCardOnAction,
PostCardFeatures,
PostCardFeaturesPreset,
PostCardFeaturesConfig,
PostCardProps,
LegacyPostCardProps,
} from './PostCard';
export { PostCardPresets, resolveFeatures } from './PostCard';
export { usePostCardActions, usePostCardFeatures } from './PostCard';
export { default as CommentItem } from './CommentItem';
export { default as UserProfileHeader } from './UserProfileHeader';
export { default as SystemMessageItem } from './SystemMessageItem';

View File

@@ -29,6 +29,7 @@ import { useUserStore } from '../../stores';
import { useCurrentUser } from '../../stores/authStore';
import { postService } from '../../services';
import { PostCard, TabBar, SearchBar } from '../../components/business';
import type { PostCardAction } from '../../components/business/PostCard';
import { Loading, EmptyState, Text, ImageGallery, ImageGridItem, ResponsiveGrid } from '../../components/common';
import { HomeStackParamList, RootStackParamList } from '../../navigation/types';
import { useResponsive, useResponsiveSpacing } from '../../hooks/useResponsive';
@@ -368,6 +369,41 @@ export const HomeScreen: React.FC = () => {
setShowImageViewer(true);
};
// 统一处理 PostCard 的所有操作
const handlePostAction = (post: Post, action: PostCardAction) => {
switch (action.type) {
case 'press':
handlePostPress(post.id);
break;
case 'userPress':
const authorId = post.author?.id;
if (authorId) handleUserPress(authorId);
break;
case 'like':
case 'unlike':
handleLike(post);
break;
case 'comment':
handlePostPress(post.id, true);
break;
case 'bookmark':
case 'unbookmark':
handleBookmark(post);
break;
case 'share':
handleShare(post);
break;
case 'imagePress':
if (action.payload?.images && action.payload?.imageIndex !== undefined) {
handleImagePress(action.payload.images, action.payload.imageIndex);
}
break;
case 'delete':
handleDeletePost(post.id);
break;
}
};
// 跳转到发帖页面(使用 Modal 方式)
const handleCreatePost = () => {
setShowCreatePost(true);
@@ -392,19 +428,12 @@ export const HomeScreen: React.FC = () => {
]}>
<PostCard
post={item}
onPress={() => handlePostPress(item.id)}
onUserPress={() => authorId && handleUserPress(authorId)}
onLike={() => handleLike(item)}
onComment={() => handlePostPress(item.id, true)}
onBookmark={() => handleBookmark(item)}
onShare={() => handleShare(item)}
onImagePress={(images, index) => handleImagePress(images, index)}
onDelete={() => handleDeletePost(item.id)}
onAction={(action) => handlePostAction(item, action)}
isPostAuthor={isPostAuthor}
/>
</View>
);
}, [currentUser?.id, handleBookmark, handleDeletePost, handleImagePress, handleLike, handlePostPress, handleShare, handleUserPress, isMobile, listItemWidth, responsiveGap]);
}, [currentUser?.id, handlePostAction, isMobile, listItemWidth, responsiveGap]);
// 估算帖子在瀑布流中的高度(用于均匀分配)
const estimatePostHeight = (post: Post, columnWidth: number): number => {

View File

@@ -22,6 +22,7 @@ import { Post } from '../../types';
import { useAuthStore, useUserStore } from '../../stores';
import { postService } from '../../services';
import { UserProfileHeader, PostCard, TabBar } from '../../components/business';
import { PostCardAction } from '../../components/business/PostCard';
import { Loading, EmptyState, Text } from '../../components/common';
import { ResponsiveContainer } from '../../components/common';
import { useResponsive } from '../../hooks';
@@ -245,6 +246,37 @@ export const ProfileScreen: React.FC = () => {
}
}, []);
// 统一的 PostCard Action 处理
const handlePostAction = useCallback((post: Post, action: PostCardAction) => {
switch (action.type) {
case 'press':
handlePostPress(post.id);
break;
case 'userPress':
if (post.author) {
handleUserPress(post.author.id);
}
break;
case 'like':
case 'unlike':
post.is_liked ? postService.unlikePost(post.id) : postService.likePost(post.id);
break;
case 'comment':
handlePostPress(post.id, true);
break;
case 'bookmark':
case 'unbookmark':
post.is_favorited ? postService.unfavoritePost(post.id) : postService.favoritePost(post.id);
break;
case 'share':
// 暂不处理分享
break;
case 'delete':
handleDeletePost(post.id);
break;
}
}, [handlePostPress, handleUserPress, handleDeletePost]);
// 渲染内容
const renderContent = useCallback(() => {
if (loading) return <Loading />;
@@ -273,13 +305,7 @@ export const ProfileScreen: React.FC = () => {
]}>
<PostCard
post={post}
onPress={() => handlePostPress(post.id)}
onUserPress={() => post.author ? handleUserPress(post.author.id) : () => {}}
onLike={() => post.is_liked ? postService.unlikePost(post.id) : postService.likePost(post.id)}
onComment={() => handlePostPress(post.id, true)}
onBookmark={() => post.is_favorited ? postService.unfavoritePost(post.id) : postService.favoritePost(post.id)}
onShare={() => {}}
onDelete={() => handleDeletePost(post.id)}
onAction={(action) => handlePostAction(post, action)}
isPostAuthor={isPostAuthor}
/>
</View>

View File

@@ -23,6 +23,7 @@ import { useCurrentUser } from '../../stores/authStore';
import { authService, postService, messageService } from '../../services';
import { userManager } from '../../stores/userManager';
import { UserProfileHeader, PostCard, TabBar } from '../../components/business';
import { PostCardAction } from '../../components/business/PostCard';
import { Loading, EmptyState, ResponsiveContainer } from '../../components/common';
import { useResponsive } from '../../hooks';
import { HomeStackParamList, RootStackParamList } from '../../navigation/types';
@@ -218,6 +219,39 @@ export const UserScreen: React.FC = () => {
}
};
// 统一处理 PostCard 的所有操作
const handlePostAction = (post: Post, action: PostCardAction) => {
switch (action.type) {
case 'press':
handlePostPress(post.id);
break;
case 'userPress':
if (post.author) handleUserPress(post.author.id);
break;
case 'like':
postService.likePost(post.id);
break;
case 'unlike':
postService.unlikePost(post.id);
break;
case 'comment':
handlePostPress(post.id, true);
break;
case 'bookmark':
postService.favoritePost(post.id);
break;
case 'unbookmark':
postService.unfavoritePost(post.id);
break;
case 'share':
// 暂不处理
break;
case 'delete':
handleDeletePost(post.id);
break;
}
};
// 跳转到聊天界面
const handleMessage = async () => {
if (!user) return;
@@ -317,13 +351,7 @@ export const UserScreen: React.FC = () => {
]}>
<PostCard
post={post}
onPress={() => handlePostPress(post.id)}
onUserPress={() => post.author ? handleUserPress(post.author.id) : () => {}}
onLike={() => post.is_liked ? postService.unlikePost(post.id) : postService.likePost(post.id)}
onComment={() => handlePostPress(post.id, true)}
onBookmark={() => post.is_favorited ? postService.unfavoritePost(post.id) : postService.favoritePost(post.id)}
onShare={() => {}}
onDelete={() => handleDeletePost(post.id)}
onAction={(action) => handlePostAction(post, action)}
isPostAuthor={isPostAuthor}
/>
</View>
@@ -357,13 +385,7 @@ export const UserScreen: React.FC = () => {
]}>
<PostCard
post={post}
onPress={() => handlePostPress(post.id)}
onUserPress={() => post.author ? handleUserPress(post.author.id) : () => {}}
onLike={() => post.is_liked ? postService.unlikePost(post.id) : postService.likePost(post.id)}
onComment={() => handlePostPress(post.id, true)}
onBookmark={() => post.is_favorited ? postService.unfavoritePost(post.id) : postService.favoritePost(post.id)}
onShare={() => {}}
onDelete={() => handleDeletePost(post.id)}
onAction={(action) => handlePostAction(post, action)}
isPostAuthor={isPostAuthor}
/>
</View>