"use client"; import { useState, useRef, useCallback, useEffect } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { X, Download, Share2, Loader2, Star, MapPin, Zap } from "lucide-react"; import { QRCodeSVG } from "qrcode.react"; import { toPng } from "html-to-image"; import type { Restaurant, MatchType, SceneType } from "@/types"; type ShareCardData = | { type: "restaurant"; restaurant: Restaurant; matchType: MatchType; matchLikes: number; userCount: number; scene?: SceneType; } | { type: "blindbox"; idea: string; submitter?: { avatar: string; username: string }; drawer?: { avatar: string; username: string }; roomName: string; }; interface ShareCardModalProps { open: boolean; onClose: () => void; data: ShareCardData; onToast?: (msg: string) => void; } async function loadImageAsDataUrl(src: string): Promise { try { const img = new Image(); img.crossOrigin = "anonymous"; img.src = src; await new Promise((resolve, reject) => { img.onload = () => resolve(); img.onerror = () => reject(); }); const canvas = document.createElement("canvas"); canvas.width = img.naturalWidth; canvas.height = img.naturalHeight; const ctx = canvas.getContext("2d")!; ctx.drawImage(img, 0, 0); return canvas.toDataURL("image/jpeg", 0.85); } catch { return null; } } async function generateImage(el: HTMLElement): Promise { return toPng(el, { pixelRatio: 2, quality: 0.95, cacheBust: true, skipAutoScale: true, filter: (node) => { if (node instanceof HTMLElement && node.dataset.shareExclude === "true") { return false; } return true; }, }); } function downloadDataUrl(dataUrl: string, filename: string) { const link = document.createElement("a"); link.download = filename; link.href = dataUrl; link.click(); } function dataUrlToFile(dataUrl: string, filename: string): File { const arr = dataUrl.split(","); const mime = arr[0].match(/:(.*?);/)?.[1] || "image/png"; const bstr = atob(arr[1]); const u8 = new Uint8Array(bstr.length); for (let i = 0; i < bstr.length; i++) u8[i] = bstr.charCodeAt(i); return new File([u8], filename, { type: mime }); } function RestaurantShareCard({ data, cardRef, imageDataUrl, }: { data: Extract; cardRef: React.RefObject; imageDataUrl: string | null; }) { const { restaurant, matchType, matchLikes, userCount, scene } = data; const isUnanimous = matchType === "unanimous"; const verb = scene === "drink" ? "喝" : "吃"; const shareUrl = typeof window !== "undefined" ? window.location.origin : "nowhatever.app"; const accentFrom = isUnanimous ? "#059669" : "#b45309"; const accentTo = isUnanimous ? "#34d399" : "#fbbf24"; const accentText = isUnanimous ? "#6ee7b7" : "#fcd34d"; const accentBg = isUnanimous ? "rgba(16, 185, 129, 0.12)" : "rgba(245, 158, 11, 0.12)"; return (
{/* Decorative glows */}
{/* Brand header */}
NoWhatever
别说随便 · PANIC MODE
{/* Thin accent line */}
{/* Hero section */}
{isUnanimous ? "🎉" : "🏆"}
就去这{verb}!
{isUnanimous && ( )} {isUnanimous ? `默契度 100% · ${userCount}人全员一致` : `${matchLikes}/${userCount} 人选了这家`} {isUnanimous && ( )}
{/* Restaurant card */}
{imageDataUrl && ( {restaurant.name} )}
{restaurant.name}
{restaurant.category && ( {restaurant.category} )}
{restaurant.rating > 0 && ( {restaurant.rating} )} {restaurant.price && restaurant.price !== "未知" && ( {restaurant.price} )} {restaurant.distance && ( {restaurant.distance} )}
{restaurant.address && (
📍 {restaurant.address}
)} {restaurant.tag && (
{restaurant.tag .split(",") .slice(0, 4) .map((t) => ( {t.trim()} ))}
)}
{/* QR footer */}
扫码一起「别说随便」
{shareUrl.replace(/^https?:\/\//, "")}
); } function BlindboxShareCard({ data, cardRef, }: { data: Extract; cardRef: React.RefObject; }) { const { idea, submitter, drawer, roomName } = data; const shareUrl = typeof window !== "undefined" ? window.location.origin : "nowhatever.app"; return (
{/* Decorative glows */}
{/* Brand header */}
🎁
NoWhatever
别说随便 · ADVENTURE ROULETTE
{/* Thin accent line */}
{/* Room name badge */}
✦ {roomName} ✦
{/* Idea card */}
{/* Corner decorations */}
{idea}
此契约一旦开启,绝不反悔
{/* Attribution */} {(submitter || drawer) && (
{submitter && (
{submitter.avatar} {submitter.username} 投入
)} {submitter && drawer && ( · )} {drawer && (
{drawer.avatar} {drawer.username} 抽中
)}
)} {/* QR footer */}
扫码一起「别说随便」
{shareUrl.replace(/^https?:\/\//, "")}
); } export default function ShareCardModal({ open, onClose, data, onToast, }: ShareCardModalProps) { const cardRef = useRef(null); const backdropRef = useRef(null); const [generating, setGenerating] = useState(false); const [imageDataUrl, setImageDataUrl] = useState(null); const [imageLoading, setImageLoading] = useState(false); useEffect(() => { if (!open) { setImageDataUrl(null); return; } if (data.type !== "restaurant") return; const src = data.restaurant.images?.[0]; if (!src) return; setImageLoading(true); loadImageAsDataUrl(src) .then(setImageDataUrl) .finally(() => setImageLoading(false)); }, [open, data]); const handleGenerate = useCallback(async (): Promise => { if (!cardRef.current) return null; setGenerating(true); try { return await generateImage(cardRef.current); } catch { try { setImageDataUrl(null); await new Promise((r) => setTimeout(r, 100)); if (!cardRef.current) return null; return await generateImage(cardRef.current); } catch { return null; } } finally { setGenerating(false); } }, []); const handleSave = useCallback(async () => { const png = await handleGenerate(); if (!png) { onToast?.("生成图片失败,请重试"); return; } const name = data.type === "restaurant" ? `NoWhatever_${data.restaurant.name}.png` : `NoWhatever_周末契约.png`; downloadDataUrl(png, name); onToast?.("图片已保存"); }, [handleGenerate, onToast, data]); const handleShare = useCallback(async () => { const png = await handleGenerate(); if (!png) { onToast?.("生成图片失败,请重试"); return; } 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; } downloadDataUrl(png, "NoWhatever.png"); onToast?.("图片已保存,快去分享吧!"); }, [handleGenerate, onToast]); const handleBackdropClick = (e: React.MouseEvent) => { if (e.target === backdropRef.current) onClose(); }; return ( {open && ( {/* Card + close button wrapper */}
{imageLoading ? (
) : data.type === "restaurant" ? ( ) : ( )}
{/* Action buttons */}

长按图片也可以保存到相册

)}
); }