feat(ui): add ShareSheet component for multi-channel post sharing
Some checks failed
Frontend CI / ota-android (push) Successful in 10m39s
Frontend CI / build-and-push-web (push) Failing after 1m29s
Frontend CI / build-android-apk (push) Has been cancelled

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:
lafay
2026-04-19 23:58:26 +08:00
parent 6910fdec70
commit b16759a147
8 changed files with 148 additions and 60 deletions

27
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "carrot_bbs",
"version": "1.0.2",
"version": "1.0.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "carrot_bbs",
"version": "1.0.2",
"version": "1.0.3",
"dependencies": {
"@expo/vector-icons": "^15.1.1",
"@react-native-async-storage/async-storage": "^2.2.0",
@@ -19,7 +19,6 @@
"axios": "^1.13.6",
"date-fns": "^4.1.0",
"expo": "~55.0.4",
"expo-background-fetch": "~55.0.10",
"expo-background-task": "~55.0.10",
"expo-camera": "^55.0.10",
"expo-constants": "~55.0.7",
@@ -52,6 +51,7 @@
"react-native-reanimated": "^4.2.1",
"react-native-safe-area-context": "~5.6.2",
"react-native-screens": "~4.23.0",
"react-native-share": "^12.2.6",
"react-native-sse": "^1.2.1",
"react-native-web": "^0.21.0",
"react-native-webrtc": "^124.0.7",
@@ -5549,18 +5549,6 @@
"react-native": "*"
}
},
"node_modules/expo-background-fetch": {
"version": "55.0.10",
"resolved": "https://registry.npmmirror.com/expo-background-fetch/-/expo-background-fetch-55.0.10.tgz",
"integrity": "sha512-JRSlQ2GEigUxKqHi11xLPz5/T02O7YzsV1xNKDOxr2SXSERmAN9WFD9l2F9SDw5tUz53ewI1kndHQ6FmSu+7Tw==",
"license": "MIT",
"dependencies": {
"expo-task-manager": "~55.0.10"
},
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-background-task": {
"version": "55.0.10",
"resolved": "https://registry.npmmirror.com/expo-background-task/-/expo-background-task-55.0.10.tgz",
@@ -9748,6 +9736,15 @@
"react-native": "*"
}
},
"node_modules/react-native-share": {
"version": "12.2.6",
"resolved": "https://registry.npmmirror.com/react-native-share/-/react-native-share-12.2.6.tgz",
"integrity": "sha512-K9jZCQaTIqSNG37kMVygU1rflVMJm2g0ikslnbbmQ7EgsckYpw7ipePyp01E64hG+HrWNl+z9ZnFWLCC6H+Tiw==",
"license": "MIT",
"engines": {
"node": ">=16"
}
},
"node_modules/react-native-sse": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/react-native-sse/-/react-native-sse-1.2.1.tgz",

View File

@@ -57,6 +57,7 @@
"react-native-reanimated": "^4.2.1",
"react-native-safe-area-context": "~5.6.2",
"react-native-screens": "~4.23.0",
"react-native-share": "^12.2.6",
"react-native-sse": "^1.2.1",
"react-native-web": "^0.21.0",
"react-native-webrtc": "^124.0.7",

View File

@@ -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) : '赞'}

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

View File

@@ -0,0 +1,2 @@
export { default } from './ShareSheet';
export type { ShareSheetProps } from './ShareSheet';

View File

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

View File

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

View File

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