diff --git a/package-lock.json b/package-lock.json index 8b4ce28..dad9e6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index b021805..c5beccb 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/business/PostCard/PostCard.tsx b/src/components/business/PostCard/PostCard.tsx index e4ecaf1..3aebc61 100644 --- a/src/components/business/PostCard/PostCard.tsx +++ b/src/components/business/PostCard/PostCard.tsx @@ -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 = (normalizedProps) => { {author?.nickname || '匿名用户'} - + {formatNumber(post.likes_count || 0)} @@ -677,9 +677,9 @@ const PostCardInner: React.FC = (normalizedProps) => { {post.likes_count > 0 ? formatNumber(post.likes_count) : '赞'} diff --git a/src/components/business/ShareSheet/ShareSheet.tsx b/src/components/business/ShareSheet/ShareSheet.tsx new file mode 100644 index 0000000..f8e9d66 --- /dev/null +++ b/src/components/business/ShareSheet/ShareSheet.tsx @@ -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 = ({ + 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; diff --git a/src/components/business/ShareSheet/index.ts b/src/components/business/ShareSheet/index.ts new file mode 100644 index 0000000..7072d3c --- /dev/null +++ b/src/components/business/ShareSheet/index.ts @@ -0,0 +1,2 @@ +export { default } from './ShareSheet'; +export type { ShareSheetProps } from './ShareSheet'; diff --git a/src/components/business/index.ts b/src/components/business/index.ts index 1269082..eb245a1 100644 --- a/src/components/business/index.ts +++ b/src/components/business/index.ts @@ -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'; diff --git a/src/screens/home/HomeScreen.tsx b/src/screens/home/HomeScreen.tsx index 8af9968..3d2e5fb 100644 --- a/src/screens/home/HomeScreen.tsx +++ b/src/screens/home/HomeScreen.tsx @@ -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(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)} /> + + {/* 分享面板 */} + { + 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); + } + })(); + } + }} + /> ); }; diff --git a/src/screens/home/PostDetailScreen.tsx b/src/screens/home/PostDetailScreen.tsx index 4919fae..65ce7f7 100644 --- a/src/screens/home/PostDetailScreen.tsx +++ b/src/screens/home/PostDetailScreen.tsx @@ -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(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); }} /> + + {/* 分享面板 */} + { + 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); + } + })(); + } + }} + /> ); }; @@ -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,