feat(ui): add ShareSheet component for multi-channel post sharing
Add ShareSheet component that provides native sharing options (WeChat, Moments, copy link, etc.) replacing the previous clipboard-only approach. Also change like icon from heart to thumb-up to better match the share functionality.
This commit is contained in:
@@ -280,10 +280,10 @@ function createPostCardStyles(colors: AppColors) {
|
||||
marginLeft: 4,
|
||||
},
|
||||
activeActionText: {
|
||||
color: colors.error.main,
|
||||
color: colors.primary.main,
|
||||
},
|
||||
activeActionTextMerged: {
|
||||
color: colors.error.main,
|
||||
color: colors.primary.main,
|
||||
fontSize: fontSizes.sm,
|
||||
marginLeft: 4,
|
||||
},
|
||||
@@ -589,7 +589,7 @@ const PostCardInner: React.FC<PostCardProps> = (normalizedProps) => {
|
||||
<Text style={styles.gridUsername} numberOfLines={1}>{author?.nickname || '匿名用户'}</Text>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.gridLikeArea}>
|
||||
<MaterialCommunityIcons name="heart-outline" size={14} color={colors.text.secondary} />
|
||||
<MaterialCommunityIcons name="thumb-up-outline" size={14} color={colors.text.secondary} />
|
||||
<Text style={styles.gridLikeCount}>{formatNumber(post.likes_count || 0)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
@@ -677,9 +677,9 @@ const PostCardInner: React.FC<PostCardProps> = (normalizedProps) => {
|
||||
<View style={styles.actionButtons}>
|
||||
<TouchableOpacity style={styles.actionBtn} onPress={handleLike}>
|
||||
<MaterialCommunityIcons
|
||||
name={post.is_liked ? 'heart' : 'heart-outline'}
|
||||
name={post.is_liked ? 'thumb-up' : 'thumb-up-outline'}
|
||||
size={18}
|
||||
color={post.is_liked ? colors.error.main : colors.text.secondary}
|
||||
color={post.is_liked ? colors.primary.main : colors.text.secondary}
|
||||
/>
|
||||
<Text style={post.is_liked ? styles.activeActionTextMerged : styles.actionText}>
|
||||
{post.likes_count > 0 ? formatNumber(post.likes_count) : '赞'}
|
||||
|
||||
55
src/components/business/ShareSheet/ShareSheet.tsx
Normal file
55
src/components/business/ShareSheet/ShareSheet.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { Share as RNShare, Platform } from 'react-native';
|
||||
|
||||
export interface ShareSheetProps {
|
||||
visible: boolean;
|
||||
postUrl?: string;
|
||||
postTitle?: string;
|
||||
onClose: () => void;
|
||||
onShareAction?: (actionKey: string) => void;
|
||||
}
|
||||
|
||||
const ShareSheet: React.FC<ShareSheetProps> = ({
|
||||
visible,
|
||||
postUrl,
|
||||
postTitle,
|
||||
onClose,
|
||||
onShareAction,
|
||||
}) => {
|
||||
|
||||
const shareText = useMemo(() => {
|
||||
const title = postTitle || '胡萝卜BBS帖子';
|
||||
return `${title} ${postUrl || ''}`;
|
||||
}, [postTitle, postUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) return;
|
||||
|
||||
const doShare = async () => {
|
||||
try {
|
||||
await RNShare.share(
|
||||
{
|
||||
message: shareText,
|
||||
url: Platform.OS === 'ios' ? postUrl : undefined,
|
||||
title: postTitle || '分享帖子',
|
||||
},
|
||||
{
|
||||
subject: postTitle,
|
||||
dialogTitle: '分享到',
|
||||
}
|
||||
);
|
||||
onShareAction?.('system_share');
|
||||
} catch {
|
||||
// User cancelled
|
||||
} finally {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
doShare();
|
||||
}, [visible, shareText, postUrl, postTitle, onShareAction, onClose]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default ShareSheet;
|
||||
2
src/components/business/ShareSheet/index.ts
Normal file
2
src/components/business/ShareSheet/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default } from './ShareSheet';
|
||||
export type { ShareSheetProps } from './ShareSheet';
|
||||
@@ -23,3 +23,4 @@ export { default as VoteCard } from './VoteCard';
|
||||
export { default as VoteEditor } from './VoteEditor';
|
||||
export { default as VotePreview } from './VotePreview';
|
||||
export { default as ReportDialog } from './ReportDialog';
|
||||
export { default as ShareSheet } from './ShareSheet';
|
||||
|
||||
@@ -29,7 +29,7 @@ import { Post } from '../../types';
|
||||
import { useUserStore, useHomeTabBarVisibilityStore, useHomeTabPressStore } from '../../stores';
|
||||
import { useCurrentUser } from '../../stores/auth';
|
||||
import { channelService, postService } from '../../services';
|
||||
import { PostCard, TabBar, SearchBar } from '../../components/business';
|
||||
import { PostCard, TabBar, SearchBar, ShareSheet } from '../../components/business';
|
||||
import type { PostCardAction } from '../../components/business/PostCard';
|
||||
import { Loading, EmptyState, Text, ImageGallery, ImageGridItem, ResponsiveGrid } from '../../components/common';
|
||||
import { useResponsive, useResponsiveSpacing } from '../../hooks/useResponsive';
|
||||
@@ -203,6 +203,10 @@ export const HomeScreen: React.FC = () => {
|
||||
// 发帖弹窗状态
|
||||
const [showCreatePost, setShowCreatePost] = useState(false);
|
||||
|
||||
// 分享面板状态
|
||||
const [showShareSheet, setShowShareSheet] = useState(false);
|
||||
const [sharePost, setSharePost] = useState<Post | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (showCreatePost) {
|
||||
blurActiveElement();
|
||||
@@ -576,20 +580,10 @@ export const HomeScreen: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 分享帖子
|
||||
const handleShare = async (post: Post) => {
|
||||
if (!post?.id) return;
|
||||
try {
|
||||
const res = await postService.sharePost(post.id, { channel: 'copy_link' });
|
||||
if (res.ok && res.shares_count != null) {
|
||||
postSyncService.applyShareCountUpdate(post.id, res.shares_count);
|
||||
}
|
||||
} catch (shareError) {
|
||||
console.error('上报分享次数失败:', shareError);
|
||||
}
|
||||
const postUrl = `https://browser.littlelan.cn/post/${encodeURIComponent(post.id)}`;
|
||||
Clipboard.setString(postUrl);
|
||||
Alert.alert('已复制', '帖子链接已复制到剪贴板');
|
||||
// 分享帖子 - 打开分享面板
|
||||
const handleShare = (post: Post) => {
|
||||
setSharePost(post);
|
||||
setShowShareSheet(true);
|
||||
};
|
||||
|
||||
// 删除帖子 - 删除后刷新列表
|
||||
@@ -1081,6 +1075,31 @@ export const HomeScreen: React.FC = () => {
|
||||
onClose={() => setShowCreatePost(false)}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
{/* 分享面板 */}
|
||||
<ShareSheet
|
||||
visible={showShareSheet}
|
||||
postUrl={sharePost ? `https://browser.littlelan.cn/post/${encodeURIComponent(sharePost.id)}` : undefined}
|
||||
postTitle={sharePost?.title}
|
||||
onClose={() => {
|
||||
setShowShareSheet(false);
|
||||
setSharePost(null);
|
||||
}}
|
||||
onShareAction={(actionKey) => {
|
||||
if (sharePost) {
|
||||
(async () => {
|
||||
try {
|
||||
const res = await postService.sharePost(sharePost.id, { channel: actionKey });
|
||||
if (res.ok && res.shares_count != null) {
|
||||
postSyncService.applyShareCountUpdate(sharePost.id, res.shares_count);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('上报分享次数失败:', e);
|
||||
}
|
||||
})();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -39,7 +39,7 @@ import { useCurrentUser } from '../../stores/auth';
|
||||
import { postService, commentService, uploadService, authService, showPrompt, voteService } from '../../services';
|
||||
import { postSyncService } from '@/services/post';
|
||||
import { useCursorPagination } from '../../hooks/useCursorPagination';
|
||||
import { CommentItem, VoteCard, ReportDialog } from '../../components/business';
|
||||
import { CommentItem, VoteCard, ReportDialog, ShareSheet } from '../../components/business';
|
||||
import { Avatar, Button, Loading, EmptyState, Text, ImageGallery, ImageGrid, ImageGridItem, AdaptiveLayout, AppBackButton } from '../../components/common';
|
||||
import { useResponsive, useResponsiveValue, useResponsiveSpacing } from '../../hooks/useResponsive';
|
||||
import { handleError } from '@/services/core';
|
||||
@@ -207,6 +207,9 @@ export const PostDetailScreen: React.FC = () => {
|
||||
// 举报相关状态
|
||||
const [reportDialogVisible, setReportDialogVisible] = useState(false);
|
||||
const [reportTarget, setReportTarget] = useState<{ type: 'post' | 'comment'; id: string } | null>(null);
|
||||
|
||||
// 分享面板状态
|
||||
const [shareSheetVisible, setShareSheetVisible] = useState(false);
|
||||
|
||||
// 投票相关状态
|
||||
const [voteResult, setVoteResult] = useState<VoteResultDTO | null>(null);
|
||||
@@ -515,25 +518,10 @@ export const PostDetailScreen: React.FC = () => {
|
||||
}
|
||||
}, [post]);
|
||||
|
||||
// 分享帖子
|
||||
// 分享帖子 - 打开分享面板
|
||||
const handleShare = useCallback(async () => {
|
||||
if (!post?.id) return;
|
||||
try {
|
||||
const res = await postService.sharePost(post.id, { channel: 'copy_link' });
|
||||
if (res.ok && res.shares_count != null) {
|
||||
setPost((prev) =>
|
||||
prev
|
||||
? { ...prev, shares_count: res.shares_count!, sharesCount: res.shares_count! }
|
||||
: null
|
||||
);
|
||||
postSyncService.applyShareCountUpdate(post.id, res.shares_count);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('上报分享次数失败:', error);
|
||||
}
|
||||
const postUrl = `https://browser.littlelan.cn/post/${encodeURIComponent(post.id)}`;
|
||||
Clipboard.setString(postUrl);
|
||||
Alert.alert('已复制', '帖子链接已复制到剪贴板');
|
||||
setShareSheetVisible(true);
|
||||
}, [post?.id]);
|
||||
|
||||
// 投票处理函数
|
||||
@@ -1757,6 +1745,35 @@ export const PostDetailScreen: React.FC = () => {
|
||||
setReportTarget(null);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 分享面板 */}
|
||||
<ShareSheet
|
||||
visible={shareSheetVisible}
|
||||
postUrl={post ? `https://browser.littlelan.cn/post/${encodeURIComponent(post.id)}` : undefined}
|
||||
postTitle={post?.title}
|
||||
onClose={() => {
|
||||
setShareSheetVisible(false);
|
||||
}}
|
||||
onShareAction={(actionKey) => {
|
||||
if (post) {
|
||||
(async () => {
|
||||
try {
|
||||
const res = await postService.sharePost(post.id, { channel: actionKey });
|
||||
if (res.ok && res.shares_count != null) {
|
||||
setPost((prev) =>
|
||||
prev
|
||||
? { ...prev, shares_count: res.shares_count!, sharesCount: res.shares_count! }
|
||||
: null
|
||||
);
|
||||
postSyncService.applyShareCountUpdate(post.id, res.shares_count!);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('上报分享次数失败:', error);
|
||||
}
|
||||
})();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -1982,18 +1999,14 @@ function createPostDetailStyles(colors: AppColors) {
|
||||
commentSectionCount: {
|
||||
marginLeft: spacing.xs,
|
||||
fontSize: fontSizes.sm,
|
||||
color: colors.text.hint,
|
||||
},
|
||||
commentSectionUnderline: {
|
||||
width: 28,
|
||||
height: 3,
|
||||
backgroundColor: colors.text.primary,
|
||||
marginTop: spacing.xs,
|
||||
borderRadius: borderRadius.xs,
|
||||
},
|
||||
commentSectionCount: {
|
||||
marginLeft: spacing.xs,
|
||||
fontSize: fontSizes.sm,
|
||||
color: colors.text.hint,
|
||||
borderRadius: borderRadius.sm,
|
||||
},
|
||||
inputContainer: {
|
||||
backgroundColor: colors.background.paper,
|
||||
@@ -2179,7 +2192,7 @@ function createPostDetailStyles(colors: AppColors) {
|
||||
height: 20,
|
||||
backgroundColor: colors.background.disabled,
|
||||
margin: 2,
|
||||
borderRadius: borderRadius.xs,
|
||||
borderRadius: borderRadius.sm,
|
||||
},
|
||||
emptyCommentsGridItemActive: {
|
||||
backgroundColor: colors.text.hint,
|
||||
|
||||
Reference in New Issue
Block a user