From a6fc523f4fa2c04d94f9ee05f4a38e4ad61f1646 Mon Sep 17 00:00:00 2001 From: kurihada Date: Tue, 24 Feb 2026 20:00:55 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E4=BA=8C=E7=BB=B4?= =?UTF-8?q?=E7=A0=81=E9=82=80=E8=AF=B7=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=89=AB?= =?UTF-8?q?=E7=A0=81=E5=8D=B3=E5=8F=AF=E5=8A=A0=E5=85=A5=E6=88=BF=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 10 +++ package.json | 1 + src/components/QrInviteModal.tsx | 139 +++++++++++++++++++++++++++++++ src/components/TopNav.tsx | 40 +++------ 4 files changed, 162 insertions(+), 28 deletions(-) create mode 100644 src/components/QrInviteModal.tsx diff --git a/package-lock.json b/package-lock.json index 983e54c..4c41f2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "lucide-react": "^0.575.0", "next": "16.1.6", "prisma": "^6.19.2", + "qrcode.react": "^4.2.0", "react": "19.2.3", "react-dom": "19.2.3", "swr": "^2.4.0" @@ -5831,6 +5832,15 @@ ], "license": "MIT" }, + "node_modules/qrcode.react": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz", + "integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==", + "license": "ISC", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", diff --git a/package.json b/package.json index ede2b3a..025c530 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "lucide-react": "^0.575.0", "next": "16.1.6", "prisma": "^6.19.2", + "qrcode.react": "^4.2.0", "react": "19.2.3", "react-dom": "19.2.3", "swr": "^2.4.0" diff --git a/src/components/QrInviteModal.tsx b/src/components/QrInviteModal.tsx new file mode 100644 index 0000000..5ffea27 --- /dev/null +++ b/src/components/QrInviteModal.tsx @@ -0,0 +1,139 @@ +"use client"; + +import { useCallback, useRef } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { QRCodeSVG } from "qrcode.react"; +import { X, Copy, Share2, QrCode } from "lucide-react"; + +interface QrInviteModalProps { + open: boolean; + onClose: () => void; + roomId: string; + onToast: (msg: string) => void; +} + +export default function QrInviteModal({ + open, + onClose, + roomId, + onToast, +}: QrInviteModalProps) { + const inviteUrl = + typeof window !== "undefined" + ? `${window.location.origin}/invite/${roomId}` + : ""; + const backdropRef = useRef(null); + + const handleBackdropClick = (e: React.MouseEvent) => { + if (e.target === backdropRef.current) onClose(); + }; + + const handleCopy = useCallback(async () => { + try { + await navigator.clipboard.writeText(inviteUrl); + onToast("邀请链接已复制,快去发给朋友吧!"); + } catch { + onToast("复制失败,请手动复制链接"); + } + }, [inviteUrl, onToast]); + + const handleShare = useCallback(async () => { + const shareData = { + title: "别说随便啦,来滑卡片决定吃什么!", + text: "我建好房间了,快点开链接一起选餐厅,滑中同一家就去吃!", + 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]); + + return ( + + {open && ( + + + + +
+
+ +

邀请饭搭子

+
+

+ 让朋友扫码加入房间,一起滑卡片选餐厅 +

+ +
+ +
+ +
+ 房间号 + + {roomId} + +
+ +
+ + +
+
+
+
+ )} +
+ ); +} diff --git a/src/components/TopNav.tsx b/src/components/TopNav.tsx index a2f2f19..3791a9f 100644 --- a/src/components/TopNav.tsx +++ b/src/components/TopNav.tsx @@ -2,8 +2,9 @@ import { useState, useCallback } from "react"; import { useRouter } from "next/navigation"; -import { Users, Share2, LogOut } from "lucide-react"; +import { Users, QrCode, LogOut } from "lucide-react"; import { motion, AnimatePresence } from "framer-motion"; +import QrInviteModal from "./QrInviteModal"; interface TopNavProps { roomId: string; @@ -13,46 +14,22 @@ interface TopNavProps { export default function TopNav({ roomId, userCount }: TopNavProps) { const router = useRouter(); const [toast, setToast] = useState(""); + const [showQr, setShowQr] = useState(false); const showToast = useCallback((msg: string) => { setToast(msg); setTimeout(() => setToast(""), 2200); }, []); - const handleInvite = useCallback(async () => { - const url = `${window.location.origin}/invite/${roomId}`; - const shareData = { - title: "别说随便啦,来滑卡片决定吃什么!", - text: "我建好房间了,快点开链接一起选餐厅,滑中同一家就去吃!", - url, - }; - - try { - if (navigator.share && navigator.canShare?.(shareData)) { - await navigator.share(shareData); - return; - } - } catch (e) { - if (e instanceof Error && e.name === "AbortError") return; - } - - try { - await navigator.clipboard.writeText(url); - showToast("邀请链接已复制,快去发给朋友吧!"); - } catch { - showToast("复制失败,请手动复制链接"); - } - }, [roomId, showToast]); - return ( <>