Your Name f874b3464f
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: add wechat pay channel to unified billing flow
2026-02-12 13:32:13 +08:00

287 lines
11 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.

import Link from "next/link";
import { Check, Rocket, ShieldCheck } from "lucide-react";
import { getCheckoutEnabledPlans, isWechatPayEnabled } from "@/lib/billing/plans";
import { MarketingShell } from "@/components/lovart/marketing-shell";
import { PricingActionButton } from "@/components/lovart/pricing-action-button";
import { WechatPricingButton } from "@/components/lovart/wechat-pricing-button";
import { Reveal } from "@/components/lovart/reveal";
import {
getEnabledAiModelsWithProvider,
getNavLinks,
getPublicFeaturedImages,
getSiteSettings,
getTutorialVideos,
} from "@/lib/db/queries";
type PricingPageProps = {
searchParams?: Promise<{ checkout?: string; plan?: string }> | { checkout?: string; plan?: string };
};
type PlanViewModel = {
id: "starter" | "basic" | "pro" | "ultimate";
name: string;
price: string;
period: string;
desc: string;
features: string[];
highlight?: boolean;
};
export default async function LovartPricingPage({ searchParams }: PricingPageProps) {
const resolvedSearchParams = await Promise.resolve(searchParams);
const checkoutState = resolvedSearchParams?.checkout;
const selectedPlan = resolvedSearchParams?.plan;
const [settings, navLinks, models, videos, featuredImages] = await Promise.all([
getSiteSettings(),
getNavLinks(),
getEnabledAiModelsWithProvider(),
getTutorialVideos(),
getPublicFeaturedImages(8),
]);
const modelCount = models.length;
const tutorialCount = videos.length;
const checkoutEnabledPlans = getCheckoutEnabledPlans();
const wechatEnabled = isWechatPayEnabled();
const plans: PlanViewModel[] = [
{
id: "starter",
name: "Starter",
price: "¥0",
period: "/月",
desc: "适合快速体验 AI 设计流程",
features: [
"基础文生图与简单编辑",
`最多 ${Math.max(3, Math.floor(modelCount / 3))} 个模型试用`,
"社区支持与基础教程",
],
},
{
id: "basic",
name: "Basic",
price: "¥69",
period: "/月",
desc: "适合独立创作者日常出图",
features: ["高清导出", "多风格模板", "历史记录与作品管理"],
},
{
id: "pro",
name: "Pro",
price: "¥199",
period: "/月",
desc: "适合高频生产内容的个人与团队",
features: [
`完整 ${modelCount} 个在线模型能力`,
`附赠 ${tutorialCount} 条教程与工作流示例`,
"优先队列与高级编辑功能",
],
highlight: true,
},
{
id: "ultimate",
name: "Ultimate",
price: "¥599",
period: "/月",
desc: "适合企业协作与品牌运营",
features: ["团队权限管理", "批量任务与自动归档", "可定制品牌资产与 SSO 接入"],
},
];
const faqs = [
{
q: "可以随时升级或降级套餐吗?",
a: "支持。套餐调整后会立刻刷新权限,已生成内容不会丢失。",
},
{
q: "定价文案和能力是固定的吗?",
a: "不是。当前页面已与后台模型和教程配置联动,后续接入支付后会直接映射到真实权益。",
},
{
q: "企业版是否支持私有部署?",
a: "支持按需评估,可在现有系统基础上扩展存储、鉴权和审计策略。",
},
];
return (
<MarketingShell
active="pricing"
bannerText={settings.bannerEnabled ? settings.bannerText : null}
navLinks={navLinks}
siteLogo={settings.siteLogo || settings.siteLogoDark}
siteName={settings.siteName || "AI Chatbot"}
stats={[
{ label: "在线模型", value: String(modelCount) },
{ label: "教程内容", value: String(tutorialCount) },
{ label: "精选样例", value: String(featuredImages.length) },
]}
>
<Reveal>
<section className="rounded-3xl border border-white/15 bg-white/[0.04] p-6 sm:p-10">
<p className="inline-flex items-center gap-2 rounded-full border border-white/20 bg-white/5 px-3 py-1 text-xs text-zinc-200">
<Rocket className="h-3.5 w-3.5 text-cyan-300" />
Pricing Blueprint
</p>
<h1 className="mt-4 text-3xl font-semibold text-white sm:text-5xl">选择你的专属方案</h1>
<p className="mt-4 max-w-2xl text-sm text-zinc-300 sm:text-base">
套餐文案已与后端模型、教程和精选样例联动。下一步接入支付接口后即可直接上线。
</p>
</section>
</Reveal>
{checkoutState ? (
<Reveal delay={0.03}>
<section className="mt-4 rounded-2xl border border-white/15 bg-white/[0.03] p-4">
{checkoutState === "success" ? (
<p className="text-sm text-emerald-200">
支付已完成,正在同步你的订阅权益{selectedPlan ? `(${selectedPlan})` : ""}。如未生效,请稍后刷新,
或前往
<Link className="ml-1 underline hover:text-emerald-100" href="/billing">
订阅中心
</Link>
查看状态。
</p>
) : (
<p className="text-sm text-amber-200">支付已取消,你可以随时重新发起订阅。</p>
)}
</section>
</Reveal>
) : null}
<Reveal delay={0.06}>
<section className="mt-8 grid gap-4 xl:grid-cols-4">
{plans.map((plan) => (
<article
key={plan.name}
className={`rounded-2xl border p-5 ${
plan.highlight
? "border-cyan-300/60 bg-gradient-to-b from-cyan-500/20 to-black/40 shadow-xl shadow-cyan-500/20"
: "border-white/15 bg-black/30"
}`}
>
<p className="text-lg font-semibold text-white">{plan.name}</p>
<div className="mt-2 flex items-end gap-1">
<span className="text-3xl font-semibold text-white">{plan.price}</span>
<span className="text-sm text-zinc-300">{plan.period}</span>
</div>
<p className="mt-2 text-sm text-zinc-300">{plan.desc}</p>
<ul className="mt-5 space-y-2">
{plan.features.map((feature) => (
<li key={feature} className="flex items-start gap-2 text-sm text-zinc-200">
<Check className="mt-0.5 h-4 w-4 text-cyan-300" />
<span>{feature}</span>
</li>
))}
</ul>
{plan.id === "starter" ? (
<Link
href="/register"
className={`mt-6 inline-flex w-full items-center justify-center rounded-xl px-4 py-2 text-sm font-semibold ${
plan.highlight
? "bg-white text-zinc-900 hover:bg-zinc-200"
: "border border-white/20 text-zinc-100 hover:border-white/40"
}`}
>
选择 {plan.name}
</Link>
) : (
<div className="mt-6 space-y-2">
<PricingActionButton
checkoutEnabled={checkoutEnabledPlans[plan.id]}
highlight={plan.highlight}
planId={plan.id}
planName={plan.name}
/>
<WechatPricingButton enabled={wechatEnabled} planId={plan.id} planName={plan.name} />
</div>
)}
</article>
))}
</section>
</Reveal>
<Reveal delay={0.12}>
<section className="mt-8 grid gap-4 lg:grid-cols-[1.1fr_1fr]">
<article className="rounded-2xl border border-white/15 bg-black/30 p-5">
<h2 className="mb-3 text-lg font-semibold text-white">计费与能力说明</h2>
<ul className="space-y-2 text-sm text-zinc-300">
<li className="flex items-start gap-2">
<ShieldCheck className="mt-0.5 h-4 w-4 text-cyan-300" />
套餐可在后台自由调整模型与功能开关,页面会自动反映实际能力。
</li>
<li className="flex items-start gap-2">
<ShieldCheck className="mt-0.5 h-4 w-4 text-cyan-300" />
当前在线模型:{modelCount},教程内容:{tutorialCount},用于驱动套餐文案。
</li>
<li className="flex items-start gap-2">
<ShieldCheck className="mt-0.5 h-4 w-4 text-cyan-300" />
已支持 Stripe + 微信支付双通道,并统一在订阅中心管理。
</li>
</ul>
</article>
<article className="rounded-2xl border border-white/15 bg-black/30 p-5">
<h2 className="mb-3 text-lg font-semibold text-white">精选样例</h2>
{featuredImages.length > 0 ? (
<div className="grid grid-cols-4 gap-2">
{featuredImages.map((image) => (
<img
key={image.id}
src={image.thumbnailUrl || image.originalUrl}
alt={image.prompt || "featured image"}
className="h-20 w-full rounded-lg object-cover"
/>
))}
</div>
) : (
<p className="text-sm text-zinc-300">暂无精选图片</p>
)}
</article>
</section>
</Reveal>
<Reveal delay={0.16}>
<section className="mt-8 rounded-2xl border border-white/15 bg-black/25 p-5">
<h2 className="mb-4 text-lg font-semibold text-white">常见问题</h2>
<div className="space-y-2">
{faqs.map((item) => (
<details key={item.q} className="group rounded-xl border border-white/10 bg-white/[0.03] px-4 py-3">
<summary className="cursor-pointer list-none text-sm font-medium text-zinc-100">
{item.q}
</summary>
<p className="mt-2 text-sm text-zinc-300">{item.a}</p>
</details>
))}
</div>
</section>
</Reveal>
<Reveal delay={0.2}>
<section className="mt-8 rounded-2xl border border-white/15 bg-white/[0.03] p-6">
<h2 className="text-2xl font-semibold text-white">用 Lovart 设计</h2>
<p className="mt-2 text-sm text-zinc-300">
目前已完成高保真定价页重建和后端联调,下一步建议直接接入订阅支付与权益同步。
</p>
<div className="mt-4 flex flex-wrap gap-3">
<Link
className="rounded-xl bg-white px-4 py-2 text-sm font-semibold text-zinc-900 hover:bg-zinc-200"
href="/register"
>
开始免费试用
</Link>
<Link
className="rounded-xl border border-white/20 px-4 py-2 text-sm text-zinc-100 hover:border-white/40"
href="/admin/settings"
>
去后台调整套餐文案
</Link>
</div>
</section>
</Reveal>
</MarketingShell>
);
}