geo/frontend/components/subscription/UpgradePrompt.tsx

253 lines
6.7 KiB
TypeScript

"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Badge } from "@/components/ui/badge";
import {
Crown,
Lock,
Sparkles,
BarChart3,
Brain,
Zap,
} from "lucide-react";
interface UpgradePromptProps {
trigger?: React.ReactNode;
feature?: string;
description?: string;
variant?: "inline" | "dialog" | "badge";
className?: string;
}
const PRO_FEATURES = [
{
icon: BarChart3,
title: "完整6维度诊断",
description: "解锁结构化数据、语义一致性、技术可访问性3个高级维度",
},
{
icon: Brain,
title: "深度竞品分析",
description: "详细对比竞品在各维度的表现,发现差异化机会",
},
{
icon: Zap,
title: "AI优化方案",
description: "基于诊断结果自动生成可执行的GEO优化建议",
},
{
icon: Sparkles,
title: "持续监控",
description: "每日自动检测品牌GEO健康分变化趋势",
},
];
export function UpgradePrompt({
trigger,
feature = "此功能",
description,
variant = "inline",
className = "",
}: UpgradePromptProps) {
const [open, setOpen] = useState(false);
if (variant === "badge") {
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Badge
variant="outline"
className={`cursor-pointer gap-1 border-amber-300 bg-amber-50 text-amber-700 hover:bg-amber-100 ${className}`}
>
<Crown className="h-3 w-3" />
Pro
</Badge>
</DialogTrigger>
<DialogContent className="sm:max-w-md">
<UpgradeDialogContent
feature={feature}
description={description}
onClose={() => setOpen(false)}
/>
</DialogContent>
</Dialog>
);
}
if (variant === "dialog") {
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
{trigger || (
<Button variant="outline" className={`gap-2 ${className}`}>
<Crown className="h-4 w-4 text-amber-500" />
Pro
</Button>
)}
</DialogTrigger>
<DialogContent className="sm:max-w-md">
<UpgradeDialogContent
feature={feature}
description={description}
onClose={() => setOpen(false)}
/>
</DialogContent>
</Dialog>
);
}
return (
<div
className={`rounded-lg border border-dashed border-amber-300 bg-amber-50/50 p-4 ${className}`}
>
<div className="flex items-start gap-3">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-amber-100">
<Lock className="h-4 w-4 text-amber-600" />
</div>
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className="font-medium text-sm">{feature}</span>
<Badge
variant="outline"
className="border-amber-300 bg-amber-100 text-amber-700 text-xs"
>
<Crown className="h-3 w-3 mr-1" />
Pro
</Badge>
</div>
<p className="text-sm text-muted-foreground">
{description || `升级Pro版即可解锁${feature}功能`}
</p>
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button
variant="outline"
size="sm"
className="mt-2 gap-1 border-amber-300 text-amber-700 hover:bg-amber-100"
>
<Crown className="h-3 w-3" />
Pro
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-md">
<UpgradeDialogContent
feature={feature}
description={description}
onClose={() => setOpen(false)}
/>
</DialogContent>
</Dialog>
</div>
</div>
</div>
);
}
function UpgradeDialogContent({
feature,
description,
onClose,
}: {
feature: string;
description?: string;
onClose: () => void;
}) {
return (
<>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Crown className="h-5 w-5 text-amber-500" />
Pro版
</DialogTitle>
<DialogDescription>
{description || `解锁${feature}等全部高级功能`}
</DialogDescription>
</DialogHeader>
<div className="space-y-3 py-4">
{PRO_FEATURES.map((feat) => {
const Icon = feat.icon;
return (
<div key={feat.title} className="flex items-start gap-3">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-primary/10">
<Icon className="h-4 w-4 text-primary" />
</div>
<div>
<p className="font-medium text-sm">{feat.title}</p>
<p className="text-xs text-muted-foreground">
{feat.description}
</p>
</div>
</div>
);
})}
</div>
<div className="flex flex-col gap-2">
<Button className="w-full gap-2" onClick={onClose}>
<Sparkles className="h-4 w-4" />
Pro
</Button>
<Button
variant="ghost"
size="sm"
className="w-full text-muted-foreground"
onClick={onClose}
>
</Button>
</div>
</>
);
}
export function UpgradeActionBadge({ className = "" }: { className?: string }) {
return (
<Badge
variant="outline"
className={`gap-1 border-amber-300 bg-amber-50 text-amber-700 text-xs ${className}`}
>
<Crown className="h-3 w-3" />
Pro
</Badge>
);
}
export function PaidActionOverlay({
children,
isPaidAction,
onUpgradeClick,
}: {
children: React.ReactNode;
isPaidAction: boolean;
onUpgradeClick?: () => void;
}) {
if (!isPaidAction) return <>{children}</>;
return (
<div className="relative">
<div className="pointer-events-none opacity-60 blur-[1px]">{children}</div>
<div className="absolute inset-0 flex items-center justify-center">
<Button
variant="outline"
size="sm"
className="gap-1 border-amber-300 bg-white text-amber-700 shadow-sm hover:bg-amber-50"
onClick={onUpgradeClick}
>
<Crown className="h-3 w-3" />
</Button>
</div>
</div>
);
}