298 lines
12 KiB
TypeScript
298 lines
12 KiB
TypeScript
import Link from "next/link";
|
||
import { ArrowRight, Bot, Crown, Film, Layers3, Sparkles, WandSparkles } from "lucide-react";
|
||
import type { ReactNode } from "react";
|
||
import { HoverStack } from "@/components/lovart/hover-stack";
|
||
import { MarketingShell } from "@/components/lovart/marketing-shell";
|
||
import { Reveal } from "@/components/lovart/reveal";
|
||
import {
|
||
getEnabledAiModelsWithProvider,
|
||
getNavLinks,
|
||
getPublicFeaturedImages,
|
||
getPublicFeaturedRanking,
|
||
getSiteSettings,
|
||
getTutorialVideos,
|
||
} from "@/lib/db/queries";
|
||
|
||
export default async function LovartHomePage() {
|
||
const [settings, navLinks, videos, models, featuredImages, ranking] = await Promise.all([
|
||
getSiteSettings(),
|
||
getNavLinks(),
|
||
getTutorialVideos(),
|
||
getEnabledAiModelsWithProvider(),
|
||
getPublicFeaturedImages(14),
|
||
getPublicFeaturedRanking(5),
|
||
]);
|
||
|
||
const siteName = settings.siteName || "AI Chatbot";
|
||
const toolboxOneLinks = navLinks.filter((link) => link.toolboxGroup === 1);
|
||
const toolboxTwoLinks = navLinks.filter((link) => link.toolboxGroup === 2);
|
||
|
||
const providerStats = Array.from(
|
||
models.reduce((acc, item) => {
|
||
const key = item.provider.name;
|
||
acc.set(key, (acc.get(key) ?? 0) + 1);
|
||
return acc;
|
||
}, new Map<string, number>())
|
||
);
|
||
|
||
const featureStories = [
|
||
{
|
||
title: "懂你所想,精准编辑",
|
||
desc: "上传参考图、文案和品牌设定,系统会自动串联模型能力并保持统一风格。",
|
||
icon: <WandSparkles className="h-4 w-4 text-cyan-200" />,
|
||
},
|
||
{
|
||
title: "每一次创作,风格始终如一",
|
||
desc: "根据当前可用模型和工具箱规则自动生成工作流,不再手动重复搭建步骤。",
|
||
icon: <Bot className="h-4 w-4 text-emerald-200" />,
|
||
},
|
||
{
|
||
title: "设计参考,贵在精准",
|
||
desc: "精选图库与作者榜实时联动后台数据,运营改动后页面立即同步。",
|
||
icon: <Crown className="h-4 w-4 text-amber-200" />,
|
||
},
|
||
];
|
||
|
||
return (
|
||
<MarketingShell
|
||
active="home"
|
||
bannerText={settings.bannerEnabled ? settings.bannerText : null}
|
||
navLinks={navLinks}
|
||
siteLogo={settings.siteLogo || settings.siteLogoDark}
|
||
siteName={siteName}
|
||
stats={[
|
||
{ label: "在线模型", value: String(models.length) },
|
||
{ label: settings.toolbox1Name || "工具箱 1", value: String(toolboxOneLinks.length) },
|
||
{ label: settings.toolbox2Name || "工具箱 2", value: String(toolboxTwoLinks.length) },
|
||
{ label: "教程内容", value: String(videos.length) },
|
||
]}
|
||
>
|
||
<Reveal>
|
||
<section className="grid gap-6 rounded-3xl border border-white/15 bg-white/[0.03] p-6 shadow-[0_18px_80px_-26px_rgba(6,182,212,0.45)] sm:p-10 lg:grid-cols-[1.16fr_1fr]">
|
||
<div className="space-y-5">
|
||
<p className="inline-flex items-center gap-2 rounded-full border border-cyan-300/25 bg-cyan-300/10 px-3 py-1 text-xs text-cyan-100">
|
||
<Sparkles className="h-3.5 w-3.5" />
|
||
Lovart Rebuild v1 · 全链路联调
|
||
</p>
|
||
|
||
<h1 className="text-3xl font-semibold leading-tight text-white sm:text-5xl">
|
||
以系统思维设计
|
||
<span className="mt-2 block bg-gradient-to-r from-cyan-300 via-blue-300 to-amber-200 bg-clip-text text-transparent">
|
||
{siteName}
|
||
</span>
|
||
</h1>
|
||
|
||
<p className="max-w-xl text-sm text-zinc-300 sm:text-base">
|
||
已连接站点配置、模型中心、精选图库、教程源与作者榜数据。
|
||
你可以直接把 `/lovart` 作为新的营销主页继续迭代细节。
|
||
</p>
|
||
|
||
<div className="flex flex-wrap items-center gap-3">
|
||
<Link
|
||
href="/register"
|
||
className="inline-flex items-center gap-2 rounded-xl bg-white px-4 py-2 text-sm font-semibold text-zinc-900 hover:bg-zinc-200"
|
||
>
|
||
立即体验
|
||
<ArrowRight className="h-4 w-4" />
|
||
</Link>
|
||
<Link
|
||
href="/lovart/pricing"
|
||
className="rounded-xl border border-white/20 px-4 py-2 text-sm text-zinc-200 hover:border-white/40 hover:text-white"
|
||
>
|
||
查看定价方案
|
||
</Link>
|
||
</div>
|
||
|
||
<div className="grid gap-2 sm:grid-cols-3">
|
||
<MetricCard icon={<Bot className="h-4 w-4" />} label="模型可用" value={String(models.length)} />
|
||
<MetricCard icon={<Layers3 className="h-4 w-4" />} label="导航入口" value={String(navLinks.length)} />
|
||
<MetricCard icon={<Film className="h-4 w-4" />} label="教程总数" value={String(videos.length)} />
|
||
</div>
|
||
</div>
|
||
|
||
<HoverStack
|
||
items={featuredImages.slice(0, 6).map((image) => ({
|
||
id: image.id,
|
||
imageUrl: image.thumbnailUrl || image.originalUrl,
|
||
modelId: image.modelId || "AI Model",
|
||
prompt: image.prompt || "AI 品牌视觉草图",
|
||
}))}
|
||
/>
|
||
</section>
|
||
</Reveal>
|
||
|
||
<Reveal delay={0.05}>
|
||
<section className="mt-10 grid gap-4 md:grid-cols-3">
|
||
{featureStories.map((item) => (
|
||
<article key={item.title} className="rounded-2xl border border-white/15 bg-black/30 p-5">
|
||
<span className="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-white/15 bg-white/[0.04]">
|
||
{item.icon}
|
||
</span>
|
||
<h2 className="mt-4 text-lg font-semibold text-white">{item.title}</h2>
|
||
<p className="mt-2 text-sm leading-6 text-zinc-300">{item.desc}</p>
|
||
</article>
|
||
))}
|
||
</section>
|
||
</Reveal>
|
||
|
||
<Reveal delay={0.09}>
|
||
<section className="mt-10 grid gap-4 lg:grid-cols-[1.05fr_1fr]">
|
||
<article className="rounded-2xl border border-white/15 bg-black/30 p-5">
|
||
<h2 className="mb-4 text-xl font-semibold text-white">模型能力分布</h2>
|
||
<div className="space-y-3">
|
||
{providerStats.length > 0 ? (
|
||
providerStats.map(([providerName, count]) => (
|
||
<div key={providerName} className="rounded-xl border border-white/10 bg-white/[0.03] p-3">
|
||
<div className="mb-2 flex items-center justify-between text-sm">
|
||
<span className="text-zinc-200">{providerName}</span>
|
||
<span className="font-medium text-white">{count}</span>
|
||
</div>
|
||
<div className="h-1.5 overflow-hidden rounded-full bg-white/10">
|
||
<div
|
||
className="h-full rounded-full bg-gradient-to-r from-cyan-300 to-blue-400"
|
||
style={{ width: `${Math.max(10, (count / Math.max(models.length, 1)) * 100)}%` }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
))
|
||
) : (
|
||
<p className="text-sm text-zinc-300">暂无可用模型</p>
|
||
)}
|
||
</div>
|
||
</article>
|
||
|
||
<article className="rounded-2xl border border-white/15 bg-black/30 p-5">
|
||
<div className="mb-4 flex items-center justify-between">
|
||
<h2 className="text-xl font-semibold text-white">精选画廊</h2>
|
||
<Link href="/featured" className="text-xs text-zinc-300 hover:text-white">
|
||
查看全部
|
||
</Link>
|
||
</div>
|
||
|
||
{featuredImages.length > 0 ? (
|
||
<div className="grid grid-cols-3 gap-2">
|
||
{featuredImages.slice(0, 9).map((image) => (
|
||
<img
|
||
key={image.id}
|
||
alt={image.prompt || "featured image"}
|
||
className="h-20 w-full rounded-lg object-cover"
|
||
src={image.thumbnailUrl || image.originalUrl}
|
||
/>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<p className="text-sm text-zinc-300">暂无精选数据</p>
|
||
)}
|
||
</article>
|
||
</section>
|
||
</Reveal>
|
||
|
||
{settings.rankingEnabled ? (
|
||
<Reveal delay={0.13}>
|
||
<section className="mt-10 rounded-2xl border border-white/15 bg-black/25 p-5">
|
||
<h2 className="mb-4 text-xl font-semibold text-white">精选作者榜</h2>
|
||
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
||
{ranking.length > 0 ? (
|
||
ranking.map((item, index) => (
|
||
<div
|
||
key={item.userId}
|
||
className="flex items-center justify-between rounded-xl border border-white/10 bg-white/[0.03] px-3 py-2"
|
||
>
|
||
<div className="flex items-center gap-2">
|
||
<span className="inline-flex h-6 w-6 items-center justify-center rounded-full bg-white/10 text-xs text-zinc-100">
|
||
{index + 1}
|
||
</span>
|
||
<div>
|
||
<p className="text-sm text-zinc-100">
|
||
{item.userName || item.userEmail?.split("@")[0] || "未知用户"}
|
||
</p>
|
||
<p className="text-xs text-zinc-400">{item.userDepartment || "未设置部门"}</p>
|
||
</div>
|
||
</div>
|
||
<span className="text-sm font-medium text-amber-200">{item.featuredCount}</span>
|
||
</div>
|
||
))
|
||
) : (
|
||
<p className="text-sm text-zinc-300">暂无榜单数据</p>
|
||
)}
|
||
</div>
|
||
</section>
|
||
</Reveal>
|
||
) : null}
|
||
|
||
{settings.tutorialEnabled ? (
|
||
<Reveal delay={0.17}>
|
||
<section className="mt-10">
|
||
<div className="mb-4 flex items-center justify-between">
|
||
<h2 className="text-xl font-semibold text-white">最新教程内容</h2>
|
||
<Link className="text-xs text-zinc-300 hover:text-white" href="/lovart/news">
|
||
查看全部动态
|
||
</Link>
|
||
</div>
|
||
|
||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||
{videos.slice(0, 6).map((video) => (
|
||
<article key={video.id} className="rounded-2xl border border-white/15 bg-white/[0.04] p-4">
|
||
<p className="line-clamp-2 text-sm font-medium text-zinc-100">{video.title}</p>
|
||
<p className="mt-2 line-clamp-2 text-xs text-zinc-400">{video.description || "视频教程"}</p>
|
||
<a
|
||
className="mt-3 inline-flex text-xs text-cyan-300 hover:text-cyan-200"
|
||
href={video.videoUrl}
|
||
rel="noreferrer"
|
||
target="_blank"
|
||
>
|
||
观看视频
|
||
</a>
|
||
</article>
|
||
))}
|
||
</div>
|
||
</section>
|
||
</Reveal>
|
||
) : null}
|
||
|
||
<Reveal delay={0.2}>
|
||
<section className="mt-10 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 max-w-2xl text-sm text-zinc-300">
|
||
当前页面已完成核心数据联调,可继续进行 1:1 交互动效精修、国际化文案和支付系统接入。
|
||
</p>
|
||
<div className="mt-4 flex flex-wrap gap-3">
|
||
<Link
|
||
href="/register"
|
||
className="rounded-xl bg-white px-4 py-2 text-sm font-semibold text-zinc-900 hover:bg-zinc-200"
|
||
>
|
||
免费开始
|
||
</Link>
|
||
<Link
|
||
href="/admin/settings"
|
||
className="rounded-xl border border-white/20 px-4 py-2 text-sm text-zinc-200 hover:border-white/40 hover:text-white"
|
||
>
|
||
打开后台继续配置
|
||
</Link>
|
||
</div>
|
||
</section>
|
||
</Reveal>
|
||
</MarketingShell>
|
||
);
|
||
}
|
||
|
||
function MetricCard({
|
||
icon,
|
||
label,
|
||
value,
|
||
}: {
|
||
icon: ReactNode;
|
||
label: string;
|
||
value: string;
|
||
}) {
|
||
return (
|
||
<div className="rounded-xl border border-white/15 bg-white/[0.03] p-3">
|
||
<p className="mb-2 inline-flex items-center gap-2 text-xs text-zinc-400">
|
||
{icon}
|
||
{label}
|
||
</p>
|
||
<p className="text-lg font-semibold text-white">{value}</p>
|
||
</div>
|
||
);
|
||
}
|