lovart3/app/lovart/page.tsx
Your Name f6b7758ec9
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: extend lovart rebuild with blog and changelog experiences
2026-02-12 12:11:47 +08:00

298 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.

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>
);
}