123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362 |
- "use client"
- import { useState, useEffect } from "react"
- import { Button } from "@/components/ui/button"
- import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
- import { Badge } from "@/components/ui/badge"
- import { CheckIcon, StarIcon, Crown } from "lucide-react"
- import { useTranslations } from "next-intl"
- import { useSession } from "next-auth/react"
- import { useRouter } from "next/navigation"
- import { toast } from "@/hooks/use-toast"
- import { useAuth } from "./providers"
- import { useToast } from "@/components/ui/use-toast"
- interface PricingSectionProps {
- locale: string
- }
- interface UserInfo {
- id: string;
- email: string;
- username: string | null;
- isEmailVerified: boolean;
- credits: number;
- subscriptionCredits: number;
- subscriptionStatus: string | null;
- subscriptionPlan: string | null;
- subscriptionStartDate: string | null;
- subscriptionEndDate: string | null;
- }
- export default function PricingSection({ locale }: PricingSectionProps) {
- const t = useTranslations("pricing")
- const { data: session, status } = useSession()
- const router = useRouter()
- const [isSubscribing, setIsSubscribing] = useState(false)
- const [userInfo, setUserInfo] = useState<UserInfo | null>(null)
- const [isLoadingUser, setIsLoadingUser] = useState(false)
- const [isLoading, setIsLoading] = useState(false)
- const { user, isLoading: isLoadingAuth, refreshUser } = useAuth()
- const { toast } = useToast()
- // 获取用户信息包括订阅状态
- useEffect(() => {
- const fetchUserInfo = async () => {
- // 等待会话状态确定后再执行
- if (status === 'loading') {
- return
- }
-
- if (!session) {
- setUserInfo(null)
- return
- }
- setIsLoadingUser(true)
- try {
- const response = await fetch('/api/auth/me', {
- method: 'GET',
- credentials: 'include',
- })
-
- if (response.ok) {
- const userData = await response.json()
- setUserInfo(userData.user)
- } else {
- setUserInfo(null)
- }
- } catch (error) {
- console.error('获取用户信息失败:', error)
- setUserInfo(null)
- } finally {
- setIsLoadingUser(false)
- }
- }
- fetchUserInfo()
- }, [session, status])
- // 监听全局认证状态变化事件
- useEffect(() => {
- const handleAuthUpdate = (event: CustomEvent) => {
- console.log('Pricing component: 收到认证状态更新事件', event.detail);
- // 当认证状态变化时,重新获取用户信息
- if (event.detail.status !== 'loading') {
- const fetchUserInfo = async () => {
- if (!event.detail.session) {
- setUserInfo(null)
- return
- }
- setIsLoadingUser(true)
- try {
- const response = await fetch('/api/auth/me', {
- method: 'GET',
- credentials: 'include',
- })
-
- if (response.ok) {
- const userData = await response.json()
- setUserInfo(userData.user)
- } else {
- setUserInfo(null)
- }
- } catch (error) {
- console.error('获取用户信息失败:', error)
- setUserInfo(null)
- } finally {
- setIsLoadingUser(false)
- }
- }
- fetchUserInfo()
- }
- }
- window.addEventListener('authStatusChanged', handleAuthUpdate as EventListener)
- return () => {
- window.removeEventListener('authStatusChanged', handleAuthUpdate as EventListener)
- }
- }, [])
- // 强制刷新用户信息的辅助函数
- const forceRefreshUserInfo = async () => {
- if (status === 'loading') return null;
-
- try {
- const response = await fetch('/api/auth/me', {
- method: 'GET',
- credentials: 'include',
- })
-
- if (response.ok) {
- const userData = await response.json()
- setUserInfo(userData.user)
- return userData.user
- }
- return null
- } catch (error) {
- console.error('刷新用户信息失败:', error)
- return null
- }
- }
- const handleSubscribe = async (priceId: string) => {
- if (isLoadingAuth) {
- toast({
- title: t('checkingAuth'),
- description: t('pleaseWait'),
- variant: 'default',
- })
- return
- }
- if (!user) {
- toast({
- title: t('loginRequired'),
- description: t('loginRequiredDesc'),
- variant: 'destructive',
- })
- router.push(`/${locale}/auth/login`)
- return
- }
- setIsLoading(true)
- try {
- const response = await fetch('/api/create-checkout-session', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- priceId,
- locale,
- }),
- })
- const data = await response.json()
- if (data.url) {
- window.location.href = data.url
- } else {
- const errorMessage = data.error === 'alreadySubscribed'
- ? t('alreadySubscribed')
- : data.error || t('createSessionFailed');
-
- toast({
- title: t('subscriptionFailed'),
- description: errorMessage,
- variant: 'destructive',
- });
- console.log('Checkout session data:', data);
-
- // 如果已经订阅,刷新用户信息
- if (data.error === 'alreadySubscribed') {
- await refreshUser();
- }
- }
- } catch (error) {
- console.error('创建订阅会话失败:', error)
- toast({
- title: t('subscriptionFailed'),
- description: t('networkError'),
- variant: 'destructive',
- })
- } finally {
- setIsLoading(false)
- }
- }
- const handleFreeSignUp = () => {
- // 跳转到demo页面
- router.push(`/${locale}#demo`);
- };
- const handleContactSales = () => {
- // 新页面打开联系方式页面
- window.open(`/${locale}/blog/contact-us`, '_blank');
- };
- const plans = [
- {
- name: t("free.name"),
- price: t("free.price"),
- originalPrice: null,
- description: t("free.description"),
- features: [t("free.feature1"), t("free.feature2"), t("free.feature3")],
- cta: t("free.cta"),
- popular: false,
- type: "free"
- },
- {
- name: t("pro.name"),
- price: t("pro.price"),
- originalPrice: t("pro.originalPrice"),
- description: t("pro.description"),
- features: [t("pro.feature1"), t("pro.feature2"), t("pro.feature3"), t("pro.feature4")],
- cta: t("subscribe"),
- popular: true,
- type: "pro"
- },
- {
- name: t("enterprise.name"),
- price: t("enterprise.price"),
- originalPrice: null,
- description: t("enterprise.description"),
- features: [t("enterprise.feature1"), t("enterprise.feature3")],
- cta: t("enterprise.cta"),
- popular: false,
- type: "enterprise"
- },
- ]
- const handleButtonClick = (planType: string) => {
- switch (planType) {
- case "free":
- handleFreeSignUp();
- break;
- case "pro":
- handleSubscribe(planType);
- break;
- case "enterprise":
- handleContactSales();
- break;
- }
- };
- return (
- <section id="pricing" className="py-20 bg-muted/30">
- <div className="container mx-auto px-4 sm:px-6 lg:px-8">
- <div className="text-center mb-16">
- <h2 className="text-3xl md:text-4xl font-bold mb-4">{t("title")}</h2>
- <p className="text-xl text-muted-foreground max-w-3xl mx-auto">{t("subtitle")}</p>
- </div>
- <div className="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-6xl mx-auto">
- {plans.map((plan, index) => (
- <Card
- key={index}
- className={`relative ${
- plan.popular ? "border-primary shadow-lg scale-105 bg-background" : "bg-background/60 backdrop-blur-sm"
- }`}
- >
- {plan.popular && (
- <div className="absolute -top-4 left-1/2 transform -translate-x-1/2">
- <Badge className="bg-gradient-to-r from-purple-600 to-blue-600 text-white px-4 py-1">
- <StarIcon className="w-4 h-4 mr-1" />
- {t("mostPopular")}
- </Badge>
- </div>
- )}
- <CardHeader className="text-center pb-8">
- <CardTitle className="text-2xl font-bold">{plan.name}</CardTitle>
- <div className="mt-4">
- {plan.originalPrice && (
- <div className="flex items-center justify-center gap-2 mb-2">
- <span className="text-lg text-muted-foreground line-through">{plan.originalPrice}</span>
- <Badge variant="destructive" className="text-xs">{t("saveAmount")}</Badge>
- </div>
- )}
- <span className="text-4xl font-bold">{plan.price}</span>
- {plan.price !== t("enterprise.price") && <span className="text-muted-foreground">{t('perMonth')}</span>}
- </div>
- <p className="text-muted-foreground mt-2">{plan.description}</p>
- </CardHeader>
- <CardContent className="space-y-6">
- <ul className="space-y-3">
- {plan.features.map((feature, featureIndex) => (
- <li key={featureIndex} className="flex items-start">
- <CheckIcon className="w-5 h-5 text-green-500 mr-3 mt-0.5 flex-shrink-0" />
- <span className="text-sm">{feature}</span>
- </li>
- ))}
- </ul>
- <Button
- className={`w-full ${
- plan.popular
- ? "bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700 text-white"
- : ""
- } ${
- plan.type === "pro" && userInfo?.subscriptionStatus === 'active'
- ? "bg-gray-400 hover:bg-gray-500 cursor-not-allowed"
- : ""
- }`}
- variant={plan.popular ? "default" : "outline"}
- size="lg"
- onClick={() => handleButtonClick(plan.type)}
- disabled={
- (plan.type === "pro" && isSubscribing) ||
- (plan.type === "pro" && userInfo?.subscriptionStatus === 'active')
- }
- >
- {plan.type === "pro" && isSubscribing ? (
- <>
- <Crown className="w-4 h-4 mr-2 animate-spin" />
- {t('processing')}
- </>
- ) : plan.type === "pro" && userInfo?.subscriptionStatus === 'active' ? (
- <>
- <Crown className="w-4 h-4 mr-2" />
- {t("subscribedPro")}
- </>
- ) : (
- <>
- {plan.type === "pro" && <Crown className="w-4 h-4 mr-2" />}
- {plan.cta}
- </>
- )}
- </Button>
- </CardContent>
- </Card>
- ))}
- </div>
- </div>
- </section>
- )
- }
|