2025-09-01 17:37:26 +08:00

445 lines
17 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">("month");
const [plans, setPlans] = useState<SubscriptionPlan[]>([]);
// 从后端获取订阅计划数据
useEffect(() => {
const loadPlans = async () => {
try {
const plansData = await fetchSubscriptionPlans();
setPlans(plansData);
} catch (err) {
console.error("加载订阅计划失败:", err);
}
};
loadPlans();
}, []);
const pricingPlans = useMemo<
{
title: string;
price: number;
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,
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) => {
try {
// 使用新的Checkout Session方案更简单
const { createCheckoutSession, redirectToCheckout } = await import(
"@/lib/stripe"
);
// 从localStorage获取当前用户信息
const User = JSON.parse(localStorage.getItem("currentUser") || "{}");
if (!User.id) {
throw new Error("Unable to obtain user ID, please log in again");
}
// 1. 创建Checkout Session
const result = await createCheckoutSession({
user_id: String(User.id),
plan_name: planName,
billing_cycle: billingType,
});
if (!result.successful || !result.data) {
throw new Error("create checkout session failed");
}
window.opener?.postMessage({ type: "waiting-payment" }, "*");
// 2. 直接跳转到Stripe托管页面就这么简单
window.location.href = result.data.checkout_url;
} catch (error) {
throw new Error("create checkout session failed, please try again later");
}
};
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
/* 小屏笔记本适配 (13-15寸) */
md:h-[1000px] md:py-16
/* 大屏笔记本适配 (16-17寸) */
lg:h-[1100px] lg:py-20
/* 桌面端适配 (21-24寸) */
xl:h-[1200px] xl:py-24
/* 大屏显示器适配 (27寸+) */
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 - <span className="text-[#FFCC6D]">20%</span>
</button>
</div>
</div>
{/* 主要价格卡片 */}
<div
className="w-full max-w-[70%] 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]"
>
<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.price}
</span>
<span
className="text-white ml-2
/* 移动端单位字体 */
text-xs
/* 平板单位字体 */
sm:text-xs
/* 小屏笔记本单位字体 */
md:text-xs
/* 大屏笔记本单位字体 */
lg:text-xs
/* 桌面端单位字体 */
xl:text-xs
/* 大屏显示器单位字体 */
2xl:text-sm"
>
/ {billingType === "month" ? "mo" : "year"}
</span>
</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]"
>
* Billed monthly until cancelled
</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>
);
}