2025-09-26 21:04:10 +08:00

342 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useState, useEffect, useMemo } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { Check, ArrowLeft } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { fetchSubscriptionPlans, SubscriptionPlan } from "@/lib/stripe";
import { DashboardLayout } from "@/components/layout/dashboard-layout";
export default function PricingPage() {
return (
<DashboardLayout>
<div className="w-full h-full overflow-y-auto bg-black text-white pb-[10rem]" id="pricing-page">
{/* Main Content */}
<HomeModule5 />
</div>
</DashboardLayout>
);
}
/**价格方案 */
function HomeModule5() {
const [billingType, setBillingType] = useState<"month" | "year">("year");
const [plans, setPlans] = useState<SubscriptionPlan[]>([]);
const [loadingPlan, setLoadingPlan] = useState<string | null>(null); // 跟踪哪个计划正在加载
// 监听支付完成消息
useEffect(() => {
const handlePaymentResult = (ev: MessageEvent) => {
if (ev.data.type === 'payment-result') {
// 支付完成后清除loading状态
setLoadingPlan(null);
}
};
window.addEventListener('message', handlePaymentResult);
return () => window.removeEventListener('message', handlePaymentResult);
}, []);
// 从后端获取订阅计划数据
useEffect(() => {
const loadPlans = async () => {
try {
const plansData = await fetchSubscriptionPlans();
setPlans(plansData);
} catch (err) {
console.error("加载订阅计划失败:", err);
}
};
loadPlans();
}, []);
const pricingPlans = useMemo<
{
title: string;
price: number;
originalPrice: number;
monthlyPrice: number;
discountMsg: string;
credits: string;
buttonText: string;
features: string[];
issubscribed: boolean;
}[]
>(() => {
return plans.map((plan) => {
return {
title: plan.display_name || plan.name,
price:
billingType === "month"
? plan.price_month / 100
: plan.price_year / 100,
originalPrice: plan.price_month / 100,
monthlyPrice: billingType === "month" ? 0 : plan.price_year / 1200,
discountMsg: `Saves $${(plan.price_month * 12 - plan.price_year) / 100} by billing yearly!`,
credits: plan.description,
buttonText: plan.is_free ? "Try For Free" : "Subscribe Now",
issubscribed: plan.is_subscribed,
features: plan.features || [],
};
});
}, [plans, billingType]);
const handleSubscribe = async (planName: string) => {
setLoadingPlan(planName);
// 改为直接携带参数打开 pay-redirect由其内部完成创建与跳转
const url = `/pay-redirect?type=subscription&plan=${encodeURIComponent(planName)}&billing=${encodeURIComponent(billingType)}`;
const win = window.open(url, '_blank');
// 通知当前窗口等待支付显示loading模态框
window.postMessage({
type: 'waiting-payment',
paymentType: 'subscription',
}, '*');
if (!win) {
setLoadingPlan(null);
throw new Error('Unable to open redirect window, please check popup settings');
}
};
return (
<div
data-alt="core-value-section"
className="home-module5 relative flex flex-col items-center justify-center w-full bg-black snap-start
min-h-[100vh] py-8
sm:min-h-[100vh] sm:py-12
md:h-[1000px] md:py-16
lg:h-[1100px] lg:py-20
xl:h-[1200px] xl:py-24
2xl:h-[1500px] 2xl:py-32"
>
<div
data-alt="core-value-content"
className="center z-10 flex flex-col items-center
mb-8 px-4
sm:mb-12 sm:px-6
md:mb-[4rem] md:px-8
lg:mb-[4rem] lg:px-12
xl:mb-[4rem] xl:px-16
2xl:mb-[4rem] 2xl:px-20"
>
<h2
className="text-white font-normal text-center
text-[1.5rem] leading-[110%] mb-4
sm:text-[2rem] sm:leading-[110%] sm:mb-6
md:text-[2.5rem] md:leading-[110%] md:mb-[1.5rem]
lg:text-[3rem] lg:leading-[110%] lg:mb-[1.5rem]
xl:text-[3.375rem] xl:leading-[110%] xl:mb-[1.5rem]
2xl:text-[3.5rem] 2xl:leading-[110%] 2xl:mb-[1.5rem]"
>
Pick a plan and make it yours
</h2>
{/* 计费切换 */}
<div
className="flex bg-black rounded-full border border-white/20
h-[2.5rem] p-[0.0625rem] mt-4
sm:h-[3rem] sm:mt-6
md:h-[3.375rem] md:mt-[1.5rem]
lg:h-[3.375rem] lg:mt-[1.5rem]
xl:h-[3.375rem] xl:mt-[1.5rem]
2xl:h-[3.375rem] 2xl:mt-[1.5rem]"
>
<button
onClick={() => setBillingType("month")}
className={`box-border flex justify-center items-center rounded-full transition-all duration-300 ${
billingType === "month"
? "bg-white text-black"
: "text-white hover:text-gray-300"
}
w-[4.5rem] text-sm
sm:w-[5rem] sm:text-sm
md:w-[6rem] md:text-base
lg:w-[6rem] lg:text-base
xl:w-[6rem] xl:text-base
2xl:w-[6rem] 2xl:text-base`}
>
Monthly
</button>
<button
onClick={() => setBillingType("year")}
className={`box-border flex justify-center items-center rounded-full transition-all duration-300 ${
billingType === "year"
? "bg-white text-black"
: "text-white hover:text-gray-300"
}
w-[5.5rem] text-sm
sm:w-[6rem] sm:text-sm
md:w-[7.125rem] md:text-base
lg:w-[7.125rem] lg:text-base
xl:w-[7.125rem] xl:text-base
2xl:w-[7.125rem] 2xl:text-base`}
>
Yearly
</button>
</div>
</div>
{/* 主要价格卡片 */}
<div
className="w-full max-w-[95%] mx-auto px-4
grid grid-cols-1 gap-4
sm:grid-cols-2 sm:gap-6 sm:px-6
md:grid-cols-3 md:gap-8 md:px-8
lg:gap-10 lg:px-12
xl:gap-12 xl:px-16
2xl:gap-16 2xl:px-20"
>
{pricingPlans.map((plan, index) => (
<div
key={index}
className="bg-black rounded-2xl border border-white/20
p-4 min-h-[28rem]
sm:p-5 sm:min-h-[32rem]
md:p-6 md:min-h-[36rem]
lg:p-[1.375rem] lg:min-h-[37rem]
xl:p-[1.5rem] xl:min-h-[38.125rem]
2xl:p-[1.75rem] 2xl:min-h-[40rem]"
>
<h3
className="text-white font-normal
text-lg mb-3
sm:text-xl sm:mb-4
md:text-xl md:mb-4
lg:text-2xl lg:mb-[1rem]
xl:text-2xl xl:mb-[1rem]
2xl:text-3xl 2xl:mb-[1.25rem]"
>
{plan.title}
</h3>
<div
className="mb-3
sm:mb-4
md:mb-4
lg:mb-[1rem]
xl:mb-[1rem]
2xl:mb-[1.25rem]"
>
<div className="flex items-baseline">
<span
className="text-white font-bold
text-2xl
sm:text-3xl
md:text-[2.5rem]
lg:text-[3rem]
xl:text-[3.375rem]
2xl:text-[3.75rem]"
>
${plan.monthlyPrice || plan.price}
</span>
<span
className="text-white ml-2 whitespace-nowrap
text-xs
sm:text-xs
md:text-xs
lg:text-xs
xl:text-xs
2xl:text-sm"
>
/ month
</span>
</div>
{plan.originalPrice !== plan.price ? (<div className="pt-2 text-white text-sm line-through">
${plan.originalPrice}
</div>) : null}
</div>
<p
className="text-white mb-4
text-sm
sm:text-sm
md:text-[0.875rem]
lg:text-[0.875rem]
xl:text-[0.875rem]
2xl:text-base"
>
{plan.credits}
</p>
{plan.issubscribed ? (
<button
disabled
className="w-full bg-gray-400 text-gray-600 rounded-full cursor-not-allowed border border-gray-300
py-2 mb-4 text-sm
sm:py-3 sm:mb-4 sm:text-base
md:py-[0.75rem] md:mb-[1rem] md:text-base
lg:py-[0.75rem] lg:mb-[1rem] lg:text-base
xl:py-[0.75rem] xl:mb-[1rem] xl:text-base
2xl:py-[0.875rem] 2xl:mb-[1.25rem] 2xl:text-lg"
>
Already Owned
</button>
) : (
<button
onClick={() => handleSubscribe(plan.title)}
className="w-full bg-white text-black rounded-full hover:bg-black hover:text-white transition-colors border border-white/20
py-2 mb-4 text-sm
sm:py-3 sm:mb-4 sm:text-base
md:py-[0.75rem] md:mb-[1rem] md:text-base
lg:py-[0.75rem] lg:mb-[1rem] lg:text-base
xl:py-[0.75rem] xl:mb-[1rem] xl:text-base
2xl:py-[0.875rem] 2xl:mb-[1.25rem] 2xl:text-lg"
>
{plan.buttonText}
</button>
)}
<p
className="w-full text-center text-white/60 mb-4
text-xs
sm:text-xs
md:text-[0.75rem] md:mb-[2rem]
lg:text-[0.75rem] lg:mb-[2rem]
xl:text-[0.75rem] xl:mb-[2rem]
2xl:text-sm 2xl:mb-[2.5rem]"
>
* {plan.discountMsg}
</p>
<ul
className="space-y-2
sm:space-y-3
md:space-y-[1rem]
lg:space-y-[1rem]
xl:space-y-[1rem]
2xl:space-y-[1.25rem]"
>
{plan.features.map((feature, featureIndex) => (
<li
key={featureIndex}
className="flex items-center text-white
text-sm
sm:text-sm
md:text-[0.875rem]
lg:text-[0.875rem]
xl:text-[0.875rem]
2xl:text-base"
>
<span
className="text-[#C73BFF] mr-2
text-sm
sm:mr-2 sm:text-base
md:mr-[0.5rem] md:text-base
lg:mr-[0.5rem] lg:text-base
xl:mr-[0.5rem] xl:text-base
2xl:mr-[0.625rem] 2xl:text-lg"
>
</span>
{feature}
</li>
))}
</ul>
</div>
))}
</div>
</div>
);
}