/** * V10 · Sprint 3 · ShareDialog * * 分享弹窗:QR Code + 链接 + 复制 + 海报生成入口 * V10 不变量:移动端 48px 触发区 */ import { useState } from 'react'; import { QRCodeSVG } from 'qrcode.react'; import { X, Copy, Check, Share2 } from 'lucide-react'; import { useShareLinkCreate, useShareLinkDelete, useShareLinkStatsQuery } from '@/hooks/useShareLink'; import { usePosterGenerateMutation } from '@/hooks/usePosterGenerate'; interface Props { planId: string; planTitle: string; open: boolean; onClose: () => void; } export function ShareDialog({ planId, planTitle, open, onClose }: Props) { const [copied, setCopied] = useState(false); const create = useShareLinkCreate(); const del = useShareLinkDelete(); const posterGen = usePosterGenerateMutation(); const code = create.data?.code ?? null; const stats = useShareLinkStatsQuery(code); if (!open) return null; const handleCreate = (): void => { create.mutate({ planId, expiresIn: 30 }); }; const handleCopy = (): void => { if (!create.data?.url) return; void navigator.clipboard .writeText(create.data.url) .then(() => { setCopied(true); setTimeout(() => setCopied(false), 2000); }) .catch(() => { // 剪贴板不可用 }); }; const handleDelete = (): void => { if (!create.data?.id) return; del.mutate(create.data.id, { onSuccess: () => create.reset(), }); }; const handleGeneratePoster = (): void => { posterGen.mutate({ planId, template: 'classic' }); }; return (
{ if (e.key === 'Escape') onClose(); }} >
e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()} >

{planTitle}

{!create.data ? (
{create.isError && (
分享链接创建失败,请稍后重试。
)}
) : (
{stats.isError && (
统计暂不可用,链接仍可正常访问。
)} {stats.data && (

访问数

{stats.data.views}

独立访客

{stats.data.uniqueVisitors}

最近访问

{stats.data.lastAccessedAt ? new Date(stats.data.lastAccessedAt).toLocaleDateString() : '—'}

)} {del.isError && (

撤销失败,请稍后重试。

)}
)}
); }