's'
This commit is contained in:
77
app/apps/[slug]/page.tsx
Normal file
77
app/apps/[slug]/page.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { AppMetaInfo, ChangelogTimeline, DownloadButtons, QRCodeSection, VersionHistory } from "@/components/app-detail";
|
||||
import { AppGrid, UnlockSection } from "@/components/sections";
|
||||
import { ScrollReveal } from "@/components/scroll-reveal";
|
||||
import { DetailScrollShowcase } from "@/components/detail-scroll-showcase";
|
||||
import { getAppBySlug, getApps } from "@/services/apps.service";
|
||||
import { ScreenshotGallery } from "@/components/screenshot-gallery";
|
||||
import { AccessGate } from "@/components/access-gate";
|
||||
|
||||
export default async function AppDetailPage({ params }: { params: Promise<{ slug: string }> }) {
|
||||
const { slug } = await params;
|
||||
const app = await getAppBySlug(slug);
|
||||
if (!app) return notFound();
|
||||
|
||||
const appResources = await getApps();
|
||||
const related = appResources.filter((i) => i.slug !== slug).slice(0, 3);
|
||||
return (
|
||||
<div className="space-y-6 py-8">
|
||||
<ScrollReveal>
|
||||
<section className="card-glass relative overflow-hidden">
|
||||
<div className="pointer-events-none absolute -right-8 -top-8 h-36 w-36 rounded-full bg-cyan-300/20 blur-3xl" />
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-4xl">{app.icon}</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">{app.name}</h1>
|
||||
<p className="text-slate-300">{app.subtitle}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{app.tags.map((t) => <span key={t} className="pill">{t}</span>)}
|
||||
</div>
|
||||
</section>
|
||||
</ScrollReveal>
|
||||
<ScrollReveal delay={0.03}>
|
||||
<AppMetaInfo app={app} />
|
||||
</ScrollReveal>
|
||||
<ScrollReveal delay={0.06}>
|
||||
<AccessGate slug={app.slug}>
|
||||
<DownloadButtons app={app} />
|
||||
</AccessGate>
|
||||
</ScrollReveal>
|
||||
<DetailScrollShowcase name={app.name} />
|
||||
<ScrollReveal delay={0.08}>
|
||||
<VersionHistory app={app} />
|
||||
</ScrollReveal>
|
||||
<ScrollReveal delay={0.1}>
|
||||
<ChangelogTimeline app={app} />
|
||||
</ScrollReveal>
|
||||
|
||||
<ScrollReveal delay={0.12}>
|
||||
<section className="card-glass">
|
||||
<h3 className="text-lg font-semibold">软件介绍</h3>
|
||||
<p className="mt-2 text-sm text-slate-300">{app.description}</p>
|
||||
<h4 className="mt-4 font-medium">使用说明</h4>
|
||||
<ul className="mt-2 list-disc space-y-1 pl-5 text-sm text-slate-300">
|
||||
{app.usage.map((u) => <li key={u}>{u}</li>)}
|
||||
</ul>
|
||||
</section>
|
||||
</ScrollReveal>
|
||||
<ScrollReveal delay={0.14}>
|
||||
<ScreenshotGallery slug={app.slug} shots={app.screenshots} />
|
||||
</ScrollReveal>
|
||||
<ScrollReveal delay={0.16}>
|
||||
<QRCodeSection />
|
||||
</ScrollReveal>
|
||||
<ScrollReveal delay={0.18}>
|
||||
<UnlockSection />
|
||||
</ScrollReveal>
|
||||
<ScrollReveal delay={0.2}>
|
||||
<section className="space-y-3">
|
||||
<h3 className="text-lg font-semibold">相关推荐</h3>
|
||||
<AppGrid apps={related} />
|
||||
</section>
|
||||
</ScrollReveal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
13
app/apps/page.tsx
Normal file
13
app/apps/page.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { AppsExplorer } from "@/components/apps-explorer";
|
||||
import { getApps } from "@/services/apps.service";
|
||||
|
||||
export default async function AppsPage() {
|
||||
const apps = await getApps();
|
||||
return (
|
||||
<div className="space-y-6 py-8">
|
||||
<h1 className="text-3xl font-bold">资源列表</h1>
|
||||
<p className="text-sm text-slate-300">支持搜索、分类筛选、排序与加载更多(本地 mock 过滤)。</p>
|
||||
<AppsExplorer apps={apps} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
43
app/checkout/page.tsx
Normal file
43
app/checkout/page.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import Link from "next/link";
|
||||
import { createCheckoutOrder } from "@/services/order.service";
|
||||
|
||||
export default async function CheckoutPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ plan?: string; slug?: string }>;
|
||||
}) {
|
||||
const { plan: rawPlan = "pro", slug = "unknown-app" } = await searchParams;
|
||||
const plan = rawPlan === "free" || rawPlan === "pro" || rawPlan === "ultra" ? rawPlan : "pro";
|
||||
const order = await createCheckoutOrder(plan, slug);
|
||||
|
||||
return (
|
||||
<div className="space-y-6 py-8">
|
||||
<section className="card-glass">
|
||||
<p className="text-xs text-cyan-300">支付确认(占位)</p>
|
||||
<h1 className="mt-1 text-3xl font-bold">订单确认</h1>
|
||||
<p className="mt-2 text-sm text-slate-300">计划:{order.plan.toUpperCase()} · 资源:{order.slug}</p>
|
||||
</section>
|
||||
|
||||
<section className="grid gap-4 md:grid-cols-2">
|
||||
<div className="card-glass space-y-2 text-sm">
|
||||
<p>订单号:{order.id}</p>
|
||||
<p>支付金额:¥{order.amount}</p>
|
||||
<p>支付方式:PayJS(预留)</p>
|
||||
<p className="text-slate-400">后续可接支付二维码、轮询支付状态与回调签名校验。</p>
|
||||
</div>
|
||||
<div className="card-glass grid place-items-center">
|
||||
<div className="grid h-44 w-44 place-items-center rounded-2xl border border-dashed border-cyan-300/40 text-xs text-slate-400">
|
||||
支付二维码占位
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="card-glass flex flex-wrap gap-2">
|
||||
<Link href={`/orders/${order.id}?status=pending&plan=${order.plan}&slug=${order.slug}`} className="cta-main">
|
||||
我已支付,查看订单状态
|
||||
</Link>
|
||||
<Link href="/pricing" className="cta-sub">返回套餐页</Link>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
BIN
app/favicon.ico
Normal file
BIN
app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
92
app/globals.css
Normal file
92
app/globals.css
Normal file
@@ -0,0 +1,92 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #050816;
|
||||
--foreground: #e6edf8;
|
||||
--card: rgba(15, 23, 42, 0.56);
|
||||
--card-border: rgba(148, 163, 184, 0.24);
|
||||
--ring: rgba(56, 189, 248, 0.45);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #020617;
|
||||
--foreground: #e2e8f0;
|
||||
}
|
||||
}
|
||||
|
||||
html.light {
|
||||
--background: #f4f7ff;
|
||||
--foreground: #111827;
|
||||
--card: rgba(255, 255, 255, 0.74);
|
||||
--card-border: rgba(148, 163, 184, 0.24);
|
||||
--ring: rgba(14, 116, 144, 0.28);
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: var(--font-geist-sans), Arial, sans-serif;
|
||||
background-image:
|
||||
radial-gradient(circle at 10% -10%, rgba(34, 211, 238, 0.18), transparent 35%),
|
||||
radial-gradient(circle at 90% 0%, rgba(139, 92, 246, 0.2), transparent 36%),
|
||||
radial-gradient(circle at 50% 100%, rgba(59, 130, 246, 0.1), transparent 45%);
|
||||
}
|
||||
|
||||
* {
|
||||
border-color: var(--card-border);
|
||||
}
|
||||
|
||||
.card-glass {
|
||||
@apply rounded-xl border p-3 backdrop-blur-xl transition duration-300;
|
||||
background: var(--card);
|
||||
border-color: var(--card-border);
|
||||
box-shadow: 0 20px 55px rgba(2, 8, 23, 0.32), inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.card-glass:hover {
|
||||
box-shadow: 0 24px 62px rgba(2, 8, 23, 0.42), 0 0 0 1px rgba(125, 211, 252, 0.16);
|
||||
}
|
||||
|
||||
.pill {
|
||||
@apply rounded-full border px-3.5 py-1.5 text-xs font-medium transition;
|
||||
background: rgba(148, 163, 184, 0.12);
|
||||
border-color: rgba(148, 163, 184, 0.24);
|
||||
}
|
||||
|
||||
.pill-active {
|
||||
@apply text-cyan-100;
|
||||
background: linear-gradient(135deg, rgba(6, 182, 212, 0.46), rgba(99, 102, 241, 0.42));
|
||||
border-color: rgba(125, 211, 252, 0.5);
|
||||
}
|
||||
|
||||
.cta-main {
|
||||
@apply rounded-xl px-5 py-2.5 text-sm font-semibold text-slate-900 transition duration-300 hover:-translate-y-0.5;
|
||||
background: linear-gradient(135deg, #22d3ee, #60a5fa 52%, #a78bfa);
|
||||
box-shadow: 0 10px 28px rgba(56, 189, 248, 0.35), 0 0 0 1px rgba(255, 255, 255, 0.2) inset;
|
||||
}
|
||||
|
||||
.cta-sub {
|
||||
@apply rounded-xl border px-5 py-2.5 text-sm transition duration-300 hover:-translate-y-0.5;
|
||||
background: rgba(15, 23, 42, 0.35);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
@apply text-lg font-semibold tracking-tight md:text-xl;
|
||||
}
|
||||
|
||||
.input-fancy {
|
||||
@apply w-full rounded-2xl border bg-slate-950/30 px-4 py-3 text-sm outline-none transition placeholder:text-slate-400;
|
||||
border-color: rgba(125, 211, 252, 0.2);
|
||||
}
|
||||
|
||||
.input-fancy:focus {
|
||||
box-shadow: 0 0 0 4px var(--ring);
|
||||
}
|
||||
44
app/insights/page.tsx
Normal file
44
app/insights/page.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { InsightChart } from "@/components/insight-chart";
|
||||
import { InsightBarChart, InsightPieChart } from "@/components/insight-extra-charts";
|
||||
|
||||
const rank = [
|
||||
["Media Spark", 29430],
|
||||
["Nova AI Studio", 28810],
|
||||
["Pixel Flow", 19840],
|
||||
["DevLight Proxy", 11690],
|
||||
];
|
||||
|
||||
export default function InsightsPage() {
|
||||
return (
|
||||
<div className="space-y-6 py-8">
|
||||
<h1 className="text-3xl font-bold">数据概览</h1>
|
||||
<p className="text-sm text-slate-300">前端 mock 展示未来分析后台能力:趋势、排行、来源、渠道。</p>
|
||||
<div className="card-glass">
|
||||
<h3 className="mb-2 text-lg font-semibold">下载趋势图</h3>
|
||||
<InsightChart />
|
||||
</div>
|
||||
<section className="grid gap-4 md:grid-cols-2">
|
||||
<div className="card-glass">
|
||||
<h3 className="mb-2 font-semibold">分类热度柱状图</h3>
|
||||
<InsightBarChart />
|
||||
</div>
|
||||
<div className="card-glass">
|
||||
<h3 className="mb-2 font-semibold">点击来源占比饼图</h3>
|
||||
<InsightPieChart />
|
||||
</div>
|
||||
</section>
|
||||
<section className="grid gap-4 md:grid-cols-2">
|
||||
<div className="card-glass">
|
||||
<h3 className="mb-2 font-semibold">热门资源排行</h3>
|
||||
{rank.map(([n, v]) => (
|
||||
<div key={n} className="mb-2 flex items-center justify-between text-sm"><span>{n}</span><span>{v}</span></div>
|
||||
))}
|
||||
</div>
|
||||
<div className="card-glass">
|
||||
<h3 className="mb-2 mt-4 font-semibold">下载渠道占比</h3>
|
||||
<p className="text-sm text-slate-300">官方 39% · 百度网盘 33% · 夸克 21% · 备用线路 7%</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
55
app/layout.tsx
Normal file
55
app/layout.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import { Navbar } from "@/components/navbar";
|
||||
import { Footer } from "@/components/sections";
|
||||
import { AmbientBackground } from "@/components/ambient-background";
|
||||
import { OpsPopup } from "@/components/ops-popup";
|
||||
import { EventDebugPanel } from "@/components/event-debug-panel";
|
||||
import { AuthProvider } from "@/components/auth-provider";
|
||||
import { DeveloperFlagsProvider } from "@/components/developer-flags-provider";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Download Nova",
|
||||
description: "高颜值下载站前端(Next.js)",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html
|
||||
lang="zh-CN"
|
||||
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<body className="min-h-full flex flex-col bg-slate-950 text-slate-100">
|
||||
<ThemeProvider>
|
||||
<DeveloperFlagsProvider>
|
||||
<AuthProvider>
|
||||
<AmbientBackground />
|
||||
<Navbar />
|
||||
<main className="mx-auto w-full max-w-7xl flex-1 px-4 md:px-6">{children}</main>
|
||||
<Footer />
|
||||
<OpsPopup />
|
||||
<EventDebugPanel />
|
||||
</AuthProvider>
|
||||
</DeveloperFlagsProvider>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
49
app/orders/[id]/page.tsx
Normal file
49
app/orders/[id]/page.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import Link from "next/link";
|
||||
import { getOrderById } from "@/services/order.service";
|
||||
import { OrderActivation } from "@/components/order-activation";
|
||||
|
||||
const statusMap = {
|
||||
pending: { label: "待支付确认", color: "text-amber-200", tips: "系统正在等待支付回调,请稍后刷新。" },
|
||||
success: { label: "支付成功", color: "text-emerald-200", tips: "会员权益已激活,可返回资源详情页下载。" },
|
||||
failed: { label: "支付失败", color: "text-rose-200", tips: "支付未完成,请重试或更换支付方式。" },
|
||||
} as const;
|
||||
|
||||
export default async function OrderStatusPage({
|
||||
params,
|
||||
searchParams,
|
||||
}: {
|
||||
params: Promise<{ id: string }>;
|
||||
searchParams: Promise<{ status?: "pending" | "success" | "failed"; plan?: string; slug?: string }>;
|
||||
}) {
|
||||
const { id } = await params;
|
||||
const { status: rawStatus = "pending", plan: rawPlan = "pro", slug = "unknown-app" } = await searchParams;
|
||||
const status = rawStatus === "pending" || rawStatus === "success" || rawStatus === "failed" ? rawStatus : "pending";
|
||||
const plan = rawPlan === "free" || rawPlan === "pro" || rawPlan === "ultra" ? rawPlan : "pro";
|
||||
const order = await getOrderById({ id, plan, slug, status });
|
||||
const info = statusMap[order.status] ?? statusMap.pending;
|
||||
|
||||
return (
|
||||
<div className="space-y-6 py-8">
|
||||
<section className="card-glass">
|
||||
<p className="text-xs text-cyan-300">订单状态页(占位)</p>
|
||||
<h1 className="mt-1 text-3xl font-bold">订单 {order.id}</h1>
|
||||
<p className={`mt-2 text-lg font-semibold ${info.color}`}>{info.label}</p>
|
||||
<p className="mt-2 text-sm text-slate-300">{info.tips}</p>
|
||||
</section>
|
||||
|
||||
<section className="card-glass text-sm text-slate-300">
|
||||
<p>套餐:{order.plan.toUpperCase()}</p>
|
||||
<p>目标资源:{order.slug}</p>
|
||||
<p>回调状态:{order.status}</p>
|
||||
<p>订单金额:¥{order.amount}</p>
|
||||
</section>
|
||||
|
||||
<section className="card-glass flex flex-wrap gap-2">
|
||||
<OrderActivation slug={order.slug} canActivate={order.status === "success"} />
|
||||
<Link href="/pricing" className="cta-sub">查看套餐页</Link>
|
||||
<Link href={`/orders/${order.id}?status=success&plan=${order.plan}&slug=${order.slug}`} className="cta-sub">模拟成功</Link>
|
||||
<Link href={`/orders/${order.id}?status=failed&plan=${order.plan}&slug=${order.slug}`} className="cta-sub">模拟失败</Link>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
85
app/page.tsx
Normal file
85
app/page.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import { categoryLabels } from "@/lib/mock-data";
|
||||
import { HeroSection, UnlockSection } from "@/components/sections";
|
||||
import { ScrollReveal } from "@/components/scroll-reveal";
|
||||
import { getApps } from "@/services/apps.service";
|
||||
import Link from "next/link";
|
||||
|
||||
export default async function Home() {
|
||||
const appResources = await getApps();
|
||||
const latest = [...appResources].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
||||
const featured = appResources.filter((a) => a.featured).slice(0, 4);
|
||||
return (
|
||||
<div className="space-y-8 py-6">
|
||||
<ScrollReveal>
|
||||
<HeroSection />
|
||||
</ScrollReveal>
|
||||
|
||||
<section className="border-y border-white/10 py-3">
|
||||
<div className="-mx-1 flex flex-wrap items-center gap-2">
|
||||
<span className="pill pill-active shrink-0">全部</span>
|
||||
{Object.values(categoryLabels).map((label) => (
|
||||
<span key={label} className="pill shrink-0">{label}</span>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid min-w-0 grid-cols-1 gap-6 lg:grid-cols-12">
|
||||
<div className="min-w-0 lg:col-span-8">
|
||||
<div className="mb-3 flex min-w-0 flex-wrap items-baseline justify-between gap-2">
|
||||
<h2 className="section-title shrink-0">最新更新</h2>
|
||||
<Link href="/apps" className="shrink-0 text-sm text-cyan-300">查看全部</Link>
|
||||
</div>
|
||||
<div className="divide-y divide-white/10 overflow-hidden rounded-xl border border-white/10">
|
||||
{latest.slice(0, 8).map((app) => (
|
||||
<Link
|
||||
key={app.id}
|
||||
href={`/apps/${app.slug}`}
|
||||
className="flex min-w-0 flex-col gap-1 px-3 py-3 transition hover:bg-white/5 sm:flex-row sm:items-center sm:gap-3"
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 items-start gap-3 sm:items-center">
|
||||
<span className="shrink-0 text-lg leading-none">{app.icon}</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm font-medium">{app.name}</p>
|
||||
<p className="truncate text-xs text-slate-400">{app.subtitle}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 flex-wrap items-center gap-x-3 gap-y-1 pl-9 text-xs text-slate-400 sm:pl-0">
|
||||
<span className="hidden whitespace-nowrap sm:inline">{app.version}</span>
|
||||
<span className="hidden whitespace-nowrap sm:inline">{app.size}</span>
|
||||
<span className="whitespace-nowrap tabular-nums">{app.updatedAt}</span>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside className="min-w-0 space-y-4 lg:col-span-4">
|
||||
<div className="rounded-xl border border-white/10 p-3">
|
||||
<p className="text-sm font-medium">精选</p>
|
||||
<div className="mt-2 space-y-2">
|
||||
{featured.map((app) => (
|
||||
<Link
|
||||
key={app.id}
|
||||
href={`/apps/${app.slug}`}
|
||||
className="flex min-w-0 items-center justify-between gap-3 text-xs text-slate-300 hover:text-white"
|
||||
>
|
||||
<span className="min-w-0 truncate">{app.name}</span>
|
||||
<span className="shrink-0 tabular-nums">{app.downloads.toLocaleString()}</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-xl border border-white/10 p-3 text-xs leading-relaxed text-slate-300">
|
||||
<p>免费资源约 70%</p>
|
||||
<p className="mt-1">解锁资源约 20%</p>
|
||||
<p className="mt-1">会员资源约 10%</p>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<ScrollReveal delay={0.04}>
|
||||
<UnlockSection />
|
||||
</ScrollReveal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
37
app/pricing/page.tsx
Normal file
37
app/pricing/page.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import Link from "next/link";
|
||||
|
||||
const plans = [
|
||||
{ name: "Free", price: "0", desc: "基础下载与公开资源", features: ["基础下载源", "公开更新日志", "每日限量"] },
|
||||
{ name: "Pro", price: "29", desc: "适合重度下载与创作者", features: ["高速下载源", "历史版本全集", "优先更新提醒"] },
|
||||
{ name: "Ultra", price: "99", desc: "团队与运营场景", features: ["多端授权", "批量下载能力", "运营面板预留接口"] },
|
||||
];
|
||||
|
||||
export default function PricingPage() {
|
||||
return (
|
||||
<div className="space-y-8 py-8">
|
||||
<section className="text-center">
|
||||
<h1 className="mt-2 text-4xl font-bold tracking-tight">会员套餐与权益</h1>
|
||||
<p className="mx-auto mt-3 max-w-2xl text-sm text-slate-300">按需选择,普通下载免费可用。</p>
|
||||
</section>
|
||||
<section className="grid gap-4 md:grid-cols-3">
|
||||
{plans.map((plan, idx) => (
|
||||
<article key={plan.name} className={`card-glass ${idx === 1 ? "border-cyan-300/40" : ""}`}>
|
||||
<p className="text-sm text-slate-300">{plan.name}</p>
|
||||
<p className="mt-2 text-4xl font-bold">¥{plan.price}<span className="text-base text-slate-400">/月</span></p>
|
||||
<p className="mt-2 text-sm text-slate-300">{plan.desc}</p>
|
||||
<ul className="mt-4 space-y-2 text-sm text-slate-200">
|
||||
{plan.features.map((f) => <li key={f}>- {f}</li>)}
|
||||
</ul>
|
||||
<Link href={`/checkout?plan=${plan.name.toLowerCase()}&slug=nova-ai-studio`} className={`mt-5 block w-full text-center ${idx === 1 ? "cta-main" : "cta-sub"}`}>
|
||||
立即开通
|
||||
</Link>
|
||||
</article>
|
||||
))}
|
||||
</section>
|
||||
<section className="card-glass flex flex-wrap items-center justify-between gap-3">
|
||||
<p className="text-sm text-slate-300">已有解锁码?可直接前往解锁中心。</p>
|
||||
<Link href="/unlock" className="cta-sub">去解锁中心</Link>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
19
app/unlock/page.tsx
Normal file
19
app/unlock/page.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { QRCodeSection } from "@/components/app-detail";
|
||||
import { UnlockWorkflow } from "@/components/unlock-workflow";
|
||||
|
||||
export default function UnlockPage() {
|
||||
return (
|
||||
<div className="space-y-6 py-8">
|
||||
<h1 className="text-4xl font-bold tracking-tight">解锁中心</h1>
|
||||
<p className="max-w-2xl text-slate-300">关注公众号并发送“下载”,即可领取解锁码。</p>
|
||||
<QRCodeSection />
|
||||
<UnlockWorkflow />
|
||||
<div className="card-glass space-y-2 text-sm text-slate-300">
|
||||
<p>1) 扫码关注公众号并回复“下载”</p>
|
||||
<p>2) 系统返回解锁码后在下载页输入</p>
|
||||
<p>3) 可加入交流群获取更新通知与新资源首发</p>
|
||||
<p>4) 领取后可解锁更多下载线路</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user