256 lines
9.1 KiB
TypeScript
256 lines
9.1 KiB
TypeScript
'use client'
|
||
|
||
import { useTranslations } from 'next-intl'
|
||
import { useAuth } from '@/hooks/useAuth'
|
||
import { useUser } from '@/hooks/useUser'
|
||
import { Header } from '@/components/layout/Header'
|
||
import { Button } from '@/components/ui/button'
|
||
import { Check, Crown, Star } from 'lucide-react'
|
||
import { SubscribeButton } from '@/components/subscription/SubscribeButton'
|
||
import { useEffect, useState } from 'react'
|
||
import type { SubscriptionPlan } from '@prisma/client'
|
||
import {
|
||
isPlanPro,
|
||
isPlanFree,
|
||
getPlanLimits,
|
||
getPlanFeatures,
|
||
formatPlanPrice,
|
||
getPlanTheme
|
||
} from '@/lib/subscription-utils'
|
||
|
||
export default function PricingPage() {
|
||
const { user } = useAuth()
|
||
const { userData } = useUser()
|
||
const t = useTranslations('pricing')
|
||
const [plans, setPlans] = useState<SubscriptionPlan[]>([])
|
||
const [loading, setLoading] = useState(true)
|
||
|
||
const fetchPlans = async () => {
|
||
try {
|
||
const response = await fetch('/api/subscription-plans')
|
||
if (response.ok) {
|
||
const data = await response.json()
|
||
// 过滤套餐:只显示真正的免费套餐、用户当前套餐,以及有 stripePriceId 的付费套餐
|
||
const filteredPlans = (data.plans || []).filter((plan: SubscriptionPlan) => {
|
||
// 只显示官方的免费套餐(ID为'free'或名称为'free')
|
||
if (isPlanFree(plan) && (plan.id === 'free' || plan.name.toLowerCase() === 'free')) {
|
||
return true
|
||
}
|
||
|
||
// 用户当前套餐总是显示(即使是异常套餐)
|
||
if (userData && isCurrentPlan(plan.id)) return true
|
||
|
||
// 付费套餐必须有 stripePriceId 才能显示(可订阅)
|
||
if (!isPlanFree(plan) && plan.stripePriceId && plan.stripePriceId.trim() !== '') {
|
||
return true
|
||
}
|
||
|
||
return false
|
||
})
|
||
setPlans(filteredPlans)
|
||
}
|
||
} catch (error) {
|
||
console.error('Error fetching plans:', error)
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
useEffect(() => {
|
||
fetchPlans()
|
||
}, [userData]) // 依赖 userData,确保用户数据加载后再过滤套餐
|
||
|
||
|
||
|
||
const isCurrentPlan = (planId: string) => {
|
||
if (!userData) return false
|
||
return userData.subscriptionPlanId === planId
|
||
}
|
||
|
||
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="min-h-screen bg-background">
|
||
<Header />
|
||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
||
<div className="text-center">
|
||
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-primary mx-auto"></div>
|
||
<p className="mt-4 text-muted-foreground">Loading pricing plans...</p>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen bg-background">
|
||
<Header />
|
||
|
||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
||
{/* Header */}
|
||
<div className="text-center mb-16">
|
||
<h1 className="text-4xl font-bold text-foreground mb-4">
|
||
{t('title')}
|
||
</h1>
|
||
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
||
{t('subtitle')}
|
||
</p>
|
||
</div>
|
||
|
||
{/* Pricing Cards */}
|
||
<div className="grid md:grid-cols-2 gap-8 max-w-4xl mx-auto">
|
||
{plans.map((plan) => {
|
||
const limits = getPlanLimits(plan)
|
||
const features = getPlanFeatures(plan)
|
||
const isFree = isPlanFree(plan)
|
||
const isPro = isPlanPro(plan)
|
||
const isCurrent = isCurrentPlan(plan.id)
|
||
const theme = getPlanTheme(plan)
|
||
|
||
return (
|
||
<div
|
||
key={plan.id}
|
||
className={`bg-card p-8 rounded-lg shadow-sm border relative ${
|
||
isPro ? `${theme.borderColor} shadow-lg` : ''
|
||
}`}
|
||
>
|
||
{isCurrent && (
|
||
<div className="absolute top-4 right-4 bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-300 px-3 py-1 rounded-full text-xs font-semibold">
|
||
{t('currentPlan')}
|
||
</div>
|
||
)}
|
||
|
||
{isPro && (
|
||
<div className="absolute top-4 left-4 bg-primary text-primary-foreground px-3 py-1 rounded-full text-xs font-semibold">
|
||
{t('popular')}
|
||
</div>
|
||
)}
|
||
|
||
<div className="text-center mb-6">
|
||
<div className="flex items-center justify-center mb-4">
|
||
<div className={`p-3 rounded-full ${theme.iconGradient}`}>
|
||
{isPro ? (
|
||
<Crown className="w-6 h-6 text-white" />
|
||
) : (
|
||
<Star className="w-6 h-6 text-white" />
|
||
)}
|
||
</div>
|
||
</div>
|
||
<h3 className="text-2xl font-bold text-card-foreground mb-2">
|
||
{plan.displayName}
|
||
</h3>
|
||
<div className="text-4xl font-bold text-card-foreground mb-2">
|
||
{formatPlanPrice(plan, t)}
|
||
</div>
|
||
<p className="text-muted-foreground">
|
||
{plan.description}
|
||
</p>
|
||
</div>
|
||
|
||
<ul className="space-y-3 mb-8">
|
||
<li className="flex items-center">
|
||
<Check className="h-5 w-5 text-green-500 mr-3" />
|
||
<span className="text-card-foreground">
|
||
{limits.promptLimit} Prompt Limit
|
||
</span>
|
||
</li>
|
||
<li className="flex items-center">
|
||
<Check className="h-5 w-5 text-green-500 mr-3" />
|
||
<span className="text-card-foreground">
|
||
{limits.maxVersionLimit} Versions per Prompt
|
||
</span>
|
||
</li>
|
||
{features.includes('prioritySupport') && (
|
||
<li className="flex items-center">
|
||
<Check className="h-5 w-5 text-green-500 mr-3" />
|
||
<span className="text-card-foreground">Priority Support</span>
|
||
</li>
|
||
)}
|
||
{features.includes('advancedAnalytics') && (
|
||
<li className="flex items-center">
|
||
<Check className="h-5 w-5 text-green-500 mr-3" />
|
||
<span className="text-card-foreground">Advanced Analytics</span>
|
||
</li>
|
||
)}
|
||
{features.includes('apiAccess') && (
|
||
<li className="flex items-center">
|
||
<Check className="h-5 w-5 text-green-500 mr-3" />
|
||
<span className="text-card-foreground">API Access</span>
|
||
</li>
|
||
)}
|
||
</ul>
|
||
|
||
{(() => {
|
||
// 免费套餐逻辑
|
||
if (isFree) {
|
||
// 如果是当前套餐,显示"当前套餐"按钮
|
||
if (isCurrent) {
|
||
return (
|
||
<Button
|
||
className="w-full"
|
||
variant="outline"
|
||
disabled
|
||
>
|
||
{t('currentPlan')}
|
||
</Button>
|
||
)
|
||
}
|
||
// 免费套餐且非当前套餐,不显示按钮
|
||
return null
|
||
}
|
||
|
||
// 付费套餐逻辑
|
||
if (isCurrent) {
|
||
// 当前套餐,显示"当前套餐"按钮
|
||
return (
|
||
<Button
|
||
className="w-full"
|
||
variant="outline"
|
||
disabled
|
||
>
|
||
{t('currentPlan')}
|
||
</Button>
|
||
)
|
||
}
|
||
|
||
// 可订阅的付费套餐,显示"立即订阅"按钮
|
||
if (plan.stripePriceId && plan.stripePriceId.trim() !== '') {
|
||
return (
|
||
<SubscribeButton
|
||
priceId={plan.stripePriceId}
|
||
planName={plan.displayName}
|
||
className={`w-full ${isPro ? 'bg-primary hover:bg-primary/90' : ''}`}
|
||
>
|
||
{t('subscribeNow')}
|
||
</SubscribeButton>
|
||
)
|
||
}
|
||
|
||
// 没有 stripePriceId 的套餐不显示按钮(理论上不会到这里,因为已经过滤了)
|
||
return null
|
||
})()}
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
|
||
{/* Additional Info */}
|
||
{user && (
|
||
<div className="text-center mt-12">
|
||
<p className="text-muted-foreground mb-4">
|
||
Need to manage your subscription?
|
||
</p>
|
||
<Button
|
||
variant="outline"
|
||
onClick={() => window.location.href = '/subscription'}
|
||
>
|
||
{t('manageSubscription')}
|
||
</Button>
|
||
</div>
|
||
)}
|
||
</main>
|
||
</div>
|
||
)
|
||
}
|