refactor: 拆分 ShareCardModal(951 行 → 4 个模块)

- ShareCardModal.tsx (199 行): 模态框编排逻辑(图片生成、保存、分享)
- RestaurantShareCard.tsx: 餐厅分享卡片纯展示组件
- BlindboxShareCard.tsx: 盲盒分享卡片纯展示组件
- shareImage.ts: 图片工具函数(toPng 封装、dataUrl 转换、下载)
This commit is contained in:
2026-02-26 19:54:12 +08:00
parent 07541ed686
commit 423b94440d
4 changed files with 776 additions and 760 deletions
+387
View File
@@ -0,0 +1,387 @@
import { Star, MapPin, Zap } from "lucide-react";
import { QRCodeSVG } from "qrcode.react";
import type { Restaurant, MatchType, SceneType } from "@/types";
export interface RestaurantShareData {
type: "restaurant";
restaurant: Restaurant;
matchType: MatchType;
matchLikes: number;
userCount: number;
scene?: SceneType;
}
export default function RestaurantShareCard({
data,
cardRef,
imageDataUrl,
}: {
data: RestaurantShareData;
cardRef: React.RefObject<HTMLDivElement | null>;
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 (
<div
ref={cardRef}
style={{
width: 340,
padding: 1.5,
borderRadius: 20,
background: `linear-gradient(160deg, ${accentFrom}, ${accentTo}40, ${accentFrom}30)`,
fontFamily:
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
}}
>
<div
style={{
borderRadius: 18.5,
background: "#08080a",
position: "relative",
overflow: "hidden",
}}
>
{/* Decorative glows */}
<div
style={{
position: "absolute",
top: -40,
right: -30,
width: 160,
height: 160,
borderRadius: "50%",
background: `radial-gradient(circle, ${accentFrom}25, transparent 70%)`,
}}
/>
<div
style={{
position: "absolute",
bottom: 60,
left: -40,
width: 120,
height: 120,
borderRadius: "50%",
background: `radial-gradient(circle, ${accentTo}12, transparent 70%)`,
}}
/>
{/* Brand header */}
<div
style={{
padding: "14px 20px 12px",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
position: "relative",
}}
>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<span style={{ fontSize: 18 }}></span>
<div>
<div
style={{
fontSize: 13,
fontWeight: 800,
color: "#ffffff",
letterSpacing: "0.02em",
}}
>
NoWhatever
</div>
<div
style={{
fontSize: 9,
fontWeight: 600,
color: "rgba(255,255,255,0.35)",
letterSpacing: "0.15em",
marginTop: 1,
}}
>
便 · PANIC MODE
</div>
</div>
</div>
</div>
{/* Thin accent line */}
<div
style={{
height: 1,
margin: "0 20px",
background: `linear-gradient(to right, transparent, ${accentTo}30, transparent)`,
}}
/>
{/* Hero section */}
<div
style={{
padding: "24px 20px 20px",
textAlign: "center",
position: "relative",
}}
>
<div style={{ fontSize: 40, lineHeight: 1 }}>
{isUnanimous ? "🎉" : "🏆"}
</div>
<div
style={{
fontSize: 30,
fontWeight: 900,
color: "#ffffff",
marginTop: 12,
letterSpacing: "0.08em",
}}
>
{verb}
</div>
<div
style={{
display: "inline-flex",
alignItems: "center",
gap: 6,
marginTop: 12,
padding: "6px 16px",
borderRadius: 100,
background: accentBg,
border: `1px solid ${accentTo}20`,
}}
>
{isUnanimous && (
<Zap
size={12}
style={{ color: accentText, fill: accentText }}
/>
)}
<span
style={{
fontSize: 11,
fontWeight: 700,
color: accentText,
}}
>
{isUnanimous
? `默契度 100% · ${userCount}人全员一致`
: `${matchLikes}/${userCount} 人选了这家`}
</span>
{isUnanimous && (
<Zap
size={12}
style={{ color: accentText, fill: accentText }}
/>
)}
</div>
</div>
{/* Restaurant card */}
<div style={{ padding: "0 16px 16px" }}>
<div
style={{
borderRadius: 14,
background: "rgba(255,255,255,0.04)",
border: "1px solid rgba(255,255,255,0.06)",
overflow: "hidden",
}}
>
{imageDataUrl && (
<img
src={imageDataUrl}
alt={restaurant.name}
style={{
width: "100%",
height: 150,
objectFit: "cover",
display: "block",
}}
/>
)}
<div style={{ padding: "14px 16px 16px" }}>
<div
style={{
display: "flex",
alignItems: "flex-start",
justifyContent: "space-between",
gap: 8,
}}
>
<div
style={{
fontSize: 17,
fontWeight: 800,
color: "#ffffff",
lineHeight: 1.3,
}}
>
{restaurant.name}
</div>
{restaurant.category && (
<span
style={{
flexShrink: 0,
padding: "3px 8px",
borderRadius: 100,
background: accentBg,
fontSize: 10,
fontWeight: 600,
color: accentText,
whiteSpace: "nowrap",
}}
>
{restaurant.category}
</span>
)}
</div>
<div
style={{
display: "flex",
alignItems: "center",
gap: 12,
marginTop: 10,
fontSize: 12,
}}
>
{restaurant.rating > 0 && (
<span
style={{
display: "flex",
alignItems: "center",
gap: 3,
color: "#e5e7eb",
fontWeight: 600,
}}
>
<Star
size={13}
style={{ color: "#fbbf24", fill: "#fbbf24" }}
/>
{restaurant.rating}
</span>
)}
{restaurant.price && restaurant.price !== "未知" && (
<span style={{ fontWeight: 700, color: accentText }}>
{restaurant.price}
</span>
)}
{restaurant.distance && (
<span
style={{
display: "flex",
alignItems: "center",
gap: 3,
color: "#9ca3af",
}}
>
<MapPin size={12} style={{ color: "#6b7280" }} />
{restaurant.distance}
</span>
)}
</div>
{restaurant.address && (
<div
style={{
marginTop: 8,
fontSize: 11,
color: "#6b7280",
lineHeight: 1.5,
}}
>
📍 {restaurant.address}
</div>
)}
{restaurant.tag && (
<div
style={{
display: "flex",
flexWrap: "wrap",
gap: 4,
marginTop: 10,
}}
>
{restaurant.tag
.split(",")
.slice(0, 4)
.map((t) => (
<span
key={t}
style={{
padding: "2px 8px",
borderRadius: 4,
background: "rgba(255,255,255,0.05)",
fontSize: 10,
fontWeight: 500,
color: "#9ca3af",
}}
>
{t.trim()}
</span>
))}
</div>
)}
</div>
</div>
</div>
{/* QR footer */}
<div
style={{
padding: "14px 20px 16px",
display: "flex",
alignItems: "center",
gap: 14,
borderTop: "1px solid rgba(255,255,255,0.04)",
}}
>
<div
style={{
padding: 5,
borderRadius: 8,
background: "#ffffff",
flexShrink: 0,
}}
>
<QRCodeSVG
value={shareUrl}
size={52}
level="M"
bgColor="#ffffff"
fgColor="#08080a"
/>
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div
style={{
fontSize: 12,
fontWeight: 700,
color: "rgba(255,255,255,0.7)",
}}
>
便
</div>
<div
style={{
fontSize: 10,
color: "rgba(255,255,255,0.25)",
marginTop: 3,
}}
>
{shareUrl.replace(/^https?:\/\//, "")}
</div>
</div>
</div>
</div>
</div>
);
}