dev #5
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "carrot_bbs",
|
"name": "carrot_bbs",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "carrot_bbs",
|
"name": "carrot_bbs",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/vector-icons": "^15.1.1",
|
"@expo/vector-icons": "^15.1.1",
|
||||||
"@react-native-async-storage/async-storage": "^2.2.0",
|
"@react-native-async-storage/async-storage": "^2.2.0",
|
||||||
|
|||||||
74
src/components/business/PostCard/PostCard.tsx
Normal file
74
src/components/business/PostCard/PostCard.tsx
Normal 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;
|
||||||
115
src/components/business/PostCard/PostCardGrid.tsx
Normal file
115
src/components/business/PostCard/PostCardGrid.tsx
Normal 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;
|
||||||
53
src/components/business/PostCard/components/PostImages.tsx
Normal file
53
src/components/business/PostCard/components/PostImages.tsx
Normal 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;
|
||||||
12
src/components/business/PostCard/components/index.ts
Normal file
12
src/components/business/PostCard/components/index.ts
Normal 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';
|
||||||
8
src/components/business/PostCard/hooks/index.ts
Normal file
8
src/components/business/PostCard/hooks/index.ts
Normal 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';
|
||||||
58
src/components/business/PostCard/index.ts
Normal file
58
src/components/business/PostCard/index.ts
Normal 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';
|
||||||
285
src/components/business/PostCard/types.ts
Normal file
285
src/components/business/PostCard/types.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -2,7 +2,21 @@
|
|||||||
* 业务组件导出
|
* 业务组件导出
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// PostCard 新架构(从 PostCard 目录导出)
|
||||||
export { default as PostCard } from './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 CommentItem } from './CommentItem';
|
||||||
export { default as UserProfileHeader } from './UserProfileHeader';
|
export { default as UserProfileHeader } from './UserProfileHeader';
|
||||||
export { default as SystemMessageItem } from './SystemMessageItem';
|
export { default as SystemMessageItem } from './SystemMessageItem';
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { useUserStore } from '../../stores';
|
|||||||
import { useCurrentUser } from '../../stores/authStore';
|
import { useCurrentUser } from '../../stores/authStore';
|
||||||
import { postService } from '../../services';
|
import { postService } from '../../services';
|
||||||
import { PostCard, TabBar, SearchBar } from '../../components/business';
|
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 { Loading, EmptyState, Text, ImageGallery, ImageGridItem, ResponsiveGrid } from '../../components/common';
|
||||||
import { HomeStackParamList, RootStackParamList } from '../../navigation/types';
|
import { HomeStackParamList, RootStackParamList } from '../../navigation/types';
|
||||||
import { useResponsive, useResponsiveSpacing } from '../../hooks/useResponsive';
|
import { useResponsive, useResponsiveSpacing } from '../../hooks/useResponsive';
|
||||||
@@ -368,6 +369,41 @@ export const HomeScreen: React.FC = () => {
|
|||||||
setShowImageViewer(true);
|
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 方式)
|
// 跳转到发帖页面(使用 Modal 方式)
|
||||||
const handleCreatePost = () => {
|
const handleCreatePost = () => {
|
||||||
setShowCreatePost(true);
|
setShowCreatePost(true);
|
||||||
@@ -392,19 +428,12 @@ export const HomeScreen: React.FC = () => {
|
|||||||
]}>
|
]}>
|
||||||
<PostCard
|
<PostCard
|
||||||
post={item}
|
post={item}
|
||||||
onPress={() => handlePostPress(item.id)}
|
onAction={(action) => handlePostAction(item, action)}
|
||||||
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)}
|
|
||||||
isPostAuthor={isPostAuthor}
|
isPostAuthor={isPostAuthor}
|
||||||
/>
|
/>
|
||||||
</View>
|
</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 => {
|
const estimatePostHeight = (post: Post, columnWidth: number): number => {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { Post } from '../../types';
|
|||||||
import { useAuthStore, useUserStore } from '../../stores';
|
import { useAuthStore, useUserStore } from '../../stores';
|
||||||
import { postService } from '../../services';
|
import { postService } from '../../services';
|
||||||
import { UserProfileHeader, PostCard, TabBar } from '../../components/business';
|
import { UserProfileHeader, PostCard, TabBar } from '../../components/business';
|
||||||
|
import { PostCardAction } from '../../components/business/PostCard';
|
||||||
import { Loading, EmptyState, Text } from '../../components/common';
|
import { Loading, EmptyState, Text } from '../../components/common';
|
||||||
import { ResponsiveContainer } from '../../components/common';
|
import { ResponsiveContainer } from '../../components/common';
|
||||||
import { useResponsive } from '../../hooks';
|
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(() => {
|
const renderContent = useCallback(() => {
|
||||||
if (loading) return <Loading />;
|
if (loading) return <Loading />;
|
||||||
@@ -273,13 +305,7 @@ export const ProfileScreen: React.FC = () => {
|
|||||||
]}>
|
]}>
|
||||||
<PostCard
|
<PostCard
|
||||||
post={post}
|
post={post}
|
||||||
onPress={() => handlePostPress(post.id)}
|
onAction={(action) => handlePostAction(post, action)}
|
||||||
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)}
|
|
||||||
isPostAuthor={isPostAuthor}
|
isPostAuthor={isPostAuthor}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { useCurrentUser } from '../../stores/authStore';
|
|||||||
import { authService, postService, messageService } from '../../services';
|
import { authService, postService, messageService } from '../../services';
|
||||||
import { userManager } from '../../stores/userManager';
|
import { userManager } from '../../stores/userManager';
|
||||||
import { UserProfileHeader, PostCard, TabBar } from '../../components/business';
|
import { UserProfileHeader, PostCard, TabBar } from '../../components/business';
|
||||||
|
import { PostCardAction } from '../../components/business/PostCard';
|
||||||
import { Loading, EmptyState, ResponsiveContainer } from '../../components/common';
|
import { Loading, EmptyState, ResponsiveContainer } from '../../components/common';
|
||||||
import { useResponsive } from '../../hooks';
|
import { useResponsive } from '../../hooks';
|
||||||
import { HomeStackParamList, RootStackParamList } from '../../navigation/types';
|
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 () => {
|
const handleMessage = async () => {
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
@@ -317,13 +351,7 @@ export const UserScreen: React.FC = () => {
|
|||||||
]}>
|
]}>
|
||||||
<PostCard
|
<PostCard
|
||||||
post={post}
|
post={post}
|
||||||
onPress={() => handlePostPress(post.id)}
|
onAction={(action) => handlePostAction(post, action)}
|
||||||
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)}
|
|
||||||
isPostAuthor={isPostAuthor}
|
isPostAuthor={isPostAuthor}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
@@ -357,13 +385,7 @@ export const UserScreen: React.FC = () => {
|
|||||||
]}>
|
]}>
|
||||||
<PostCard
|
<PostCard
|
||||||
post={post}
|
post={post}
|
||||||
onPress={() => handlePostPress(post.id)}
|
onAction={(action) => handlePostAction(post, action)}
|
||||||
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)}
|
|
||||||
isPostAuthor={isPostAuthor}
|
isPostAuthor={isPostAuthor}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
Reference in New Issue
Block a user