"use client"; import { useState, useRef, useEffect } from "react"; import { HERO_CONFIG, BENEFITS_CONFIG, CURRICULUM_CONFIG, CTA_CONFIG, INSTRUCTORS_CONFIG, } from "@/config/course"; type Props = { title?: string; url?: string; className?: string }; const POSTER_WIDTH = 750; const POSTER_HEIGHT = 1334; /** 使用公共 API 生成二维码图片,无需 npm 包 */ async function getQRImage(url: string, size: number): Promise { const encoded = encodeURIComponent(url); const apiUrl = `https://api.qrserver.com/v1/create-qr-code/?size=${size}x${size}&data=${encoded}&format=png&bgcolor=0f172a&color=22d3ee`; return new Promise((resolve) => { const img = new Image(); img.crossOrigin = "anonymous"; img.onload = () => resolve(img); img.onerror = () => resolve(null); img.src = apiUrl; }); } function wrapText( ctx: CanvasRenderingContext2D, text: string, x: number, y: number, maxWidth: number, lineHeight: number ): number { const words = text.split(""); let line = ""; let lineCount = 0; for (const char of words) { const test = line + char; const m = ctx.measureText(test); if (m.width > maxWidth && line) { ctx.fillText(line, x, y + lineCount * lineHeight); lineCount++; line = char; } else { line = test; } } if (line) { ctx.fillText(line, x, y + lineCount * lineHeight); lineCount++; } return lineCount * lineHeight; } export function ShareButton({ title = HERO_CONFIG.title, url, className = "" }: Props) { const [open, setOpen] = useState(false); const [ready, setReady] = useState(false); const canvasRef = useRef(null); const getShareUrl = () => url || (typeof window !== "undefined" ? window.location.origin : ""); const drawPoster = async () => { const canvas = canvasRef.current; if (!canvas || typeof window === "undefined") return; const ctx = canvas.getContext("2d"); if (!ctx) return; const shareUrl = getShareUrl(); const dpr = Math.min(window.devicePixelRatio || 1, 2); canvas.width = POSTER_WIDTH * dpr; canvas.height = POSTER_HEIGHT * dpr; ctx.scale(dpr, dpr); const w = POSTER_WIDTH; const h = POSTER_HEIGHT; const cyan = "#22d3ee"; const cyanDim = "rgba(34,211,238,0.6)"; const white = "#f8fafc"; const muted = "rgba(248,250,252,0.7)"; const dark = "#0f172a"; const cardBg = "rgba(15,23,42,0.8)"; // ========== 背景:深色科技感 ========== const bgGrad = ctx.createLinearGradient(0, 0, w, h); bgGrad.addColorStop(0, "#020617"); bgGrad.addColorStop(0.4, "#0f172a"); bgGrad.addColorStop(0.8, "#0c4a6e"); bgGrad.addColorStop(1, "#0e7490"); ctx.fillStyle = bgGrad; ctx.fillRect(0, 0, w, h); // 网格线 ctx.strokeStyle = "rgba(34,211,238,0.06)"; ctx.lineWidth = 1; const gridStep = 40; for (let i = 0; i <= w; i += gridStep) { ctx.beginPath(); ctx.moveTo(i, 0); ctx.lineTo(i, h); ctx.stroke(); } for (let i = 0; i <= h; i += gridStep) { ctx.beginPath(); ctx.moveTo(0, i); ctx.lineTo(w, i); ctx.stroke(); } // 顶部光晕 const glow = ctx.createRadialGradient(w / 2, 0, 0, w / 2, 0, 400); glow.addColorStop(0, "rgba(34,211,238,0.15)"); glow.addColorStop(0.5, "rgba(34,211,238,0.04)"); glow.addColorStop(1, "transparent"); ctx.fillStyle = glow; ctx.fillRect(0, 0, w, 350); // 底部光晕 const glowBot = ctx.createRadialGradient(w / 2, h, 0, w / 2, h, 350); glowBot.addColorStop(0, "rgba(34,211,238,0.12)"); glowBot.addColorStop(1, "transparent"); ctx.fillStyle = glowBot; ctx.fillRect(0, h - 400, w, 400); // ========== 顶部:标题区 ========== ctx.fillStyle = cyan; ctx.font = "bold 38px sans-serif"; ctx.textAlign = "center"; ctx.fillText("🌍 " + title, w / 2, 88); ctx.font = "20px sans-serif"; ctx.fillStyle = muted; ctx.textAlign = "left"; wrapText(ctx, HERO_CONFIG.subtitle, 48, 128, 654, 26); ctx.textAlign = "center"; // 分隔线 ctx.strokeStyle = "rgba(34,211,238,0.3)"; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(60, 168); ctx.lineTo(w - 60, 168); ctx.stroke(); // ========== 数据卡片 ========== const cardY = 192; const cardW = 210; const cardH = 68; const cardGap = (w - cardW * 3) / 4; HERO_CONFIG.stats.forEach((s, i) => { const x = cardGap + i * (cardW + cardGap); ctx.fillStyle = cardBg; ctx.strokeStyle = "rgba(34,211,238,0.25)"; ctx.lineWidth = 1; ctx.beginPath(); ctx.roundRect(x, cardY, cardW, cardH, 10); ctx.fill(); ctx.stroke(); ctx.fillStyle = cyan; ctx.font = "bold 26px sans-serif"; ctx.textAlign = "center"; ctx.fillText(s.value, x + cardW / 2, cardY + 36); ctx.font = "13px sans-serif"; ctx.fillStyle = muted; ctx.fillText(s.label, x + cardW / 2, cardY + 56); }); ctx.textAlign = "center"; // ========== 学完你将获得 ========== let y = 290; ctx.font = "bold 18px sans-serif"; ctx.fillStyle = white; ctx.fillText(BENEFITS_CONFIG.title, w / 2, y); y += 28; ctx.font = "13px sans-serif"; ctx.fillStyle = muted; ctx.fillText(BENEFITS_CONFIG.subtitle, w / 2, y); y += 32; BENEFITS_CONFIG.items.forEach((item) => { ctx.fillStyle = cardBg; ctx.strokeStyle = "rgba(34,211,238,0.15)"; ctx.lineWidth = 1; ctx.beginPath(); ctx.roundRect(48, y, w - 96, 40, 8); ctx.fill(); ctx.stroke(); ctx.fillStyle = cyanDim; ctx.font = "15px sans-serif"; ctx.textAlign = "left"; ctx.fillText(`${item.num}. ${item.title}`, 64, y + 26); ctx.textAlign = "center"; y += 48; }); // ========== 课程模块 ========== y += 16; ctx.font = "bold 18px sans-serif"; ctx.fillStyle = white; ctx.fillText(CURRICULUM_CONFIG.title, w / 2, y); y += 26; ctx.font = "13px sans-serif"; ctx.fillStyle = muted; ctx.fillText(CURRICULUM_CONFIG.subtitle, w / 2, y); y += 32; const cols = 2; const modW = (w - 96 - 12) / cols; CURRICULUM_CONFIG.modules.forEach((m, i) => { const col = i % cols; const row = Math.floor(i / cols); const mx = 48 + col * (modW + 12); const my = y + row * 44; ctx.fillStyle = cardBg; ctx.strokeStyle = "rgba(34,211,238,0.12)"; ctx.lineWidth = 1; ctx.beginPath(); ctx.roundRect(mx, my, modW, 38, 6); ctx.fill(); ctx.stroke(); ctx.fillStyle = white; ctx.font = "14px sans-serif"; ctx.textAlign = "left"; ctx.fillText(`${m.emoji} ${m.title}`, mx + 12, my + 24); ctx.textAlign = "center"; }); y += Math.ceil(CURRICULUM_CONFIG.modules.length / cols) * 44 + 20; // ========== 徽章 ========== ctx.font = "15px sans-serif"; ctx.fillStyle = cyanDim; ctx.fillText("✓ " + CTA_CONFIG.badges.join(" · "), w / 2, y); y += 44; // ========== 导师 ========== ctx.font = "13px sans-serif"; ctx.fillStyle = muted; ctx.fillText(INSTRUCTORS_CONFIG.subtitle, w / 2, y); y += 32; // ========== 二维码区 ========== const qrSize = 200; const qrX = (w - qrSize) / 2; const qrY = y; const qrImg = await getQRImage(shareUrl, qrSize); if (qrImg) { ctx.fillStyle = cardBg; ctx.strokeStyle = "rgba(34,211,238,0.4)"; ctx.lineWidth = 2; ctx.beginPath(); ctx.roundRect(qrX - 10, qrY - 10, qrSize + 20, qrSize + 20, 12); ctx.fill(); ctx.stroke(); ctx.drawImage(qrImg, qrX, qrY, qrSize, qrSize); } else { ctx.fillStyle = cardBg; ctx.fillRect(qrX, qrY, qrSize, qrSize); ctx.fillStyle = cyan; ctx.font = "13px sans-serif"; ctx.fillText(shareUrl, w / 2, qrY + qrSize / 2 + 5); } y += qrSize + 24; ctx.font = "bold 18px sans-serif"; ctx.fillStyle = cyan; ctx.fillText("扫码访问课程", w / 2, y); y += 24; ctx.font = "14px sans-serif"; ctx.fillStyle = muted; ctx.fillText("长按识别二维码", w / 2, y); y += 28; ctx.font = "12px sans-serif"; ctx.fillStyle = "rgba(248,250,252,0.5)"; ctx.fillText("自由工作 · 自在生活", w / 2, y); setReady(true); }; useEffect(() => { if (open && canvasRef.current) { setReady(false); drawPoster(); } }, [open, url]); useEffect(() => { const onKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") setOpen(false); }; if (open) { window.addEventListener("keydown", onKeyDown); document.body.style.overflow = "hidden"; } return () => { window.removeEventListener("keydown", onKeyDown); document.body.style.overflow = ""; }; }, [open]); const handleDownload = () => { const canvas = canvasRef.current; if (!canvas || !ready) return; const link = document.createElement("a"); link.download = `分享-${title}.png`; link.href = canvas.toDataURL("image/png"); link.click(); }; return ( <> {open && (
setOpen(false)} >
e.stopPropagation()} >

分享海报

下载海报后,可通过微信、朋友圈等渠道分享

)} ); }