diff --git a/src/app/blindbox/[code]/page.tsx b/src/app/blindbox/[code]/page.tsx index 57e2e1b..ea3220e 100644 --- a/src/app/blindbox/[code]/page.tsx +++ b/src/app/blindbox/[code]/page.tsx @@ -25,6 +25,7 @@ import { getCachedProfile, isRegistered } from "@/lib/userId"; import ShareCardModal from "@/components/ShareCardModal"; import Button from "@/components/Button"; import { useToast } from "@/hooks/useToast"; +import { useShare } from "@/hooks/useShare"; import { BlindboxRoomSkeleton } from "@/components/Skeleton"; import type { UserProfile } from "@/types"; @@ -391,32 +392,21 @@ export default function BlindboxRoomPage() { setTimeout(frame, 200); }; - const handleCopyCode = async () => { - if (!room) return; - try { - await navigator.clipboard.writeText(room.code); - toast.show("房间号已复制"); - } catch { /* ignore */ } - }; + const { share, copyToClipboard } = useShare(); - const handleShare = async () => { + const handleCopyCode = useCallback( + () => room ? copyToClipboard(room.code, "房间号已复制") : undefined, + [room, copyToClipboard], + ); + + const handleShare = useCallback(() => { if (!room) return; const url = typeof window !== "undefined" ? `${window.location.origin}/blindbox/${room.code}` : ""; - const shareData = { - title: `周末契约 · ${room.name}`, - text: `来和我一起玩周末盲盒吧!房间号:${room.code}`, - url, - }; - try { - if (navigator.share && navigator.canShare?.(shareData)) { - await navigator.share(shareData); - return; - } - } catch (e) { - if (e instanceof Error && e.name === "AbortError") return; - } - handleCopyCode(); - }; + share( + { title: `周末契约 · ${room.name}`, text: `来和我一起玩周末盲盒吧!房间号:${room.code}`, url }, + handleCopyCode, + ); + }, [room, share, handleCopyCode]); const isCreator = profile?.id === room?.creatorId; diff --git a/src/components/QrInviteModal.tsx b/src/components/QrInviteModal.tsx index cb48300..211efa1 100644 --- a/src/components/QrInviteModal.tsx +++ b/src/components/QrInviteModal.tsx @@ -7,7 +7,7 @@ import type { SceneType } from "@/types"; import { getSceneConfig } from "@/lib/sceneConfig"; import Modal from "@/components/Modal"; import Button from "@/components/Button"; -import { useToast } from "@/hooks/useToast"; +import { useShare } from "@/hooks/useShare"; interface QrInviteModalProps { open: boolean; @@ -22,40 +22,22 @@ export default function QrInviteModal({ roomId, scene = "eat", }: QrInviteModalProps) { - const toast = useToast(); + const { share, copyToClipboard } = useShare(); const sceneConfig = getSceneConfig(scene); const inviteUrl = typeof window !== "undefined" ? `${window.location.origin}/invite/${roomId}` : ""; - const handleCopy = useCallback(async () => { - try { - await navigator.clipboard.writeText(inviteUrl); - toast.show("邀请链接已复制,快去发给朋友吧!"); - } catch { - toast.show("复制失败,请手动复制链接"); - } - }, [inviteUrl, toast]); + const handleCopy = useCallback( + () => copyToClipboard(inviteUrl, "邀请链接已复制,快去发给朋友吧!"), + [inviteUrl, copyToClipboard], + ); - const handleShare = useCallback(async () => { - const shareData = { - title: sceneConfig.shareTitle, - text: sceneConfig.shareText, - url: inviteUrl, - }; - - try { - if (navigator.share && navigator.canShare?.(shareData)) { - await navigator.share(shareData); - return; - } - } catch (e) { - if (e instanceof Error && e.name === "AbortError") return; - } - - handleCopy(); - }, [inviteUrl, handleCopy, sceneConfig]); + const handleShare = useCallback( + () => share({ title: sceneConfig.shareTitle, text: sceneConfig.shareText, url: inviteUrl }, handleCopy), + [inviteUrl, sceneConfig, share, handleCopy], + ); return ( diff --git a/src/components/ShareCardModal.tsx b/src/components/ShareCardModal.tsx index 8c6da4e..acff296 100644 --- a/src/components/ShareCardModal.tsx +++ b/src/components/ShareCardModal.tsx @@ -7,6 +7,7 @@ import { QRCodeSVG } from "qrcode.react"; import { toPng } from "html-to-image"; import type { Restaurant, MatchType, SceneType } from "@/types"; import { useToast } from "@/hooks/useToast"; +import { useShare } from "@/hooks/useShare"; type ShareCardData = | { @@ -832,6 +833,8 @@ export default function ShareCardModal({ toast.show("图片已保存"); }, [handleGenerate, toast, data]); + const { share: nativeShare } = useShare(); + const handleShare = useCallback(async () => { const png = await handleGenerate(); if (!png) { @@ -840,20 +843,12 @@ export default function ShareCardModal({ } const file = dataUrlToFile(png, "NoWhatever.png"); - - const shareData: ShareData = { files: [file] }; - try { - if (navigator.canShare?.(shareData)) { - await navigator.share(shareData); - return; - } - } catch (e) { - if (e instanceof Error && e.name === "AbortError") return; + const shared = await nativeShare({ files: [file] }); + if (!shared) { + downloadDataUrl(png, "NoWhatever.png"); + toast.show("图片已保存,快去分享吧!"); } - - downloadDataUrl(png, "NoWhatever.png"); - toast.show("图片已保存,快去分享吧!"); - }, [handleGenerate, toast]); + }, [handleGenerate, toast, nativeShare]); const handleBackdropClick = (e: React.MouseEvent) => { if (e.target === backdropRef.current) onClose(); diff --git a/src/hooks/useShare.ts b/src/hooks/useShare.ts new file mode 100644 index 0000000..b95966e --- /dev/null +++ b/src/hooks/useShare.ts @@ -0,0 +1,39 @@ +"use client"; + +import { useCallback } from "react"; +import { useToast } from "@/hooks/useToast"; + +export function useShare() { + const toast = useToast(); + + const copyToClipboard = useCallback( + async (text: string, successMsg = "已复制") => { + try { + await navigator.clipboard.writeText(text); + toast.show(successMsg); + } catch { + toast.show("复制失败,请手动复制"); + } + }, + [toast], + ); + + /** Try the native Web Share API; calls `fallback` if unavailable. Returns true if native share was triggered. */ + const share = useCallback( + async (data: ShareData, fallback?: () => void): Promise => { + try { + if (navigator.share && navigator.canShare?.(data)) { + await navigator.share(data); + return true; + } + } catch (e) { + if (e instanceof Error && e.name === "AbortError") return true; + } + fallback?.(); + return false; + }, + [], + ); + + return { share, copyToClipboard }; +}