diff --git a/app/api/public/lovart/route.ts b/app/api/public/lovart/route.ts
index 159d58d..b3c058e 100644
--- a/app/api/public/lovart/route.ts
+++ b/app/api/public/lovart/route.ts
@@ -1,5 +1,6 @@
import { type NextRequest, NextResponse } from "next/server";
import {
+ getPublicChangelogEvents,
getEnabledAiModelsWithProvider,
getNavLinks,
getPublicFeaturedImages,
@@ -67,6 +68,41 @@ export async function GET(request: NextRequest) {
return response;
}
+ if (view === "blog") {
+ const featuredImages = await getPublicFeaturedImages(8);
+
+ const response = NextResponse.json({
+ view,
+ settings,
+ blog: {
+ videos,
+ featuredImages,
+ },
+ });
+
+ response.headers.set("Cache-Control", "public, s-maxage=60, stale-while-revalidate=300");
+ return response;
+ }
+
+ if (view === "changelog") {
+ const [events, ranking] = await Promise.all([
+ getPublicChangelogEvents(18),
+ settings.rankingEnabled ? getPublicFeaturedRanking(6) : Promise.resolve([]),
+ ]);
+
+ const response = NextResponse.json({
+ view,
+ settings,
+ changelog: {
+ events,
+ ranking,
+ },
+ });
+
+ response.headers.set("Cache-Control", "public, s-maxage=60, stale-while-revalidate=300");
+ return response;
+ }
+
const [featuredImages, ranking] = await Promise.all([
getPublicFeaturedImages(12),
settings.rankingEnabled ? getPublicFeaturedRanking(5) : Promise.resolve([]),
diff --git a/app/lovart/blog/page.tsx b/app/lovart/blog/page.tsx
new file mode 100644
index 0000000..a83c753
--- /dev/null
+++ b/app/lovart/blog/page.tsx
@@ -0,0 +1,148 @@
+import Link from "next/link";
+import { BookOpen, Calendar, PlayCircle } from "lucide-react";
+import { MarketingShell } from "@/components/lovart/marketing-shell";
+import { Reveal } from "@/components/lovart/reveal";
+import {
+ getNavLinks,
+ getPublicFeaturedImages,
+ getSiteSettings,
+ getTutorialVideos,
+} from "@/lib/db/queries";
+
+function formatDate(value: Date | null | undefined) {
+ if (!value) {
+ return "最近更新";
+ }
+
+ return new Intl.DateTimeFormat("zh-CN", {
+ year: "numeric",
+ month: "2-digit",
+ day: "2-digit",
+ }).format(new Date(value));
+}
+
+export default async function LovartBlogPage() {
+ const [settings, navLinks, videos, featuredImages] = await Promise.all([
+ getSiteSettings(),
+ getNavLinks(),
+ getTutorialVideos(),
+ getPublicFeaturedImages(8),
+ ]);
+
+ const [headline, ...restVideos] = videos;
+
+ return (
+
+
+
+
+
+ Design Blog
+
+ Lovart 博客
+
+ 这里聚合后台教程与案例内容,可作为对外 Blog 页持续运营更新。
+
+
+
+
+
+
+
+
+
+ {formatDate(headline?.updatedAt || headline?.createdAt)}
+
+ {headline?.title || "等待发布首篇内容"}
+
+ {headline?.description || "可在后台维护教程标题、描述与链接,自动同步到博客页。"}
+
+ {headline ? (
+
+
+ 阅读/观看
+
+ ) : null}
+
+
+
+ 灵感墙
+ {featuredImages.length > 0 ? (
+
+ {featuredImages.slice(0, 6).map((image) => (
+

+ ))}
+
+ ) : (
+ 暂无灵感素材
+ )}
+
+
+
+
+
+
+ 全部文章
+
+ {restVideos.length > 0 ? (
+ restVideos.map((video) => (
+
+ {formatDate(video.updatedAt || video.createdAt)}
+ {video.title}
+ {video.description || "暂无描述"}
+
+
+ 查看详情
+
+
+ ))
+ ) : (
+
+ 暂无更多文章内容。
+
+ )}
+
+
+
+
+
+
+ 继续联调
+
+
+ 查看更新日志
+
+
+ 后台更新内容
+
+
+
+
+
+ );
+}
diff --git a/app/lovart/changelog/page.tsx b/app/lovart/changelog/page.tsx
new file mode 100644
index 0000000..7888bc8
--- /dev/null
+++ b/app/lovart/changelog/page.tsx
@@ -0,0 +1,152 @@
+import Link from "next/link";
+import { Calendar, WandSparkles } from "lucide-react";
+import { MarketingShell } from "@/components/lovart/marketing-shell";
+import { Reveal } from "@/components/lovart/reveal";
+import {
+ getNavLinks,
+ getPublicChangelogEvents,
+ getPublicFeaturedRanking,
+ getSiteSettings,
+} from "@/lib/db/queries";
+
+function formatDate(value: Date | null | undefined) {
+ if (!value) {
+ return "最近更新";
+ }
+
+ return new Intl.DateTimeFormat("zh-CN", {
+ year: "numeric",
+ month: "2-digit",
+ day: "2-digit",
+ }).format(new Date(value));
+}
+
+export default async function LovartChangelogPage() {
+ const [settings, navLinks, events, ranking] = await Promise.all([
+ getSiteSettings(),
+ getNavLinks(),
+ getPublicChangelogEvents(18),
+ getPublicFeaturedRanking(6),
+ ]);
+
+ const tutorialCount = events.filter((event) => event.kind === "tutorial").length;
+ const featuredCount = events.filter((event) => event.kind === "featured").length;
+
+ return (
+
+
+
+
+
+ Product Changelog
+
+ 更新日志
+
+ 自动聚合教程发布与精选作品上新,形成可追踪的产品更新时间线。
+
+
+
+
+
+
+
+ 近期更新时间线
+ {events.length > 0 ? (
+
+ {events.map((event) => (
+
+
+
+
+ {formatDate(event.occurredAt)}
+
+
+ {event.kind}
+
+
+
{event.title}
+
{event.summary}
+
+
{event.detail}
+ {event.href ? (
+
+ 查看
+
+ ) : null}
+
+
+ ))}
+
+ ) : (
+ 暂无更新记录
+ )}
+
+
+
+ 精选作者榜
+
+ {ranking.length > 0 ? (
+ ranking.map((item, index) => (
+
+
+ {index + 1}. {item.userName || item.userEmail?.split("@")[0] || "未知用户"}
+
+ {item.featuredCount}
+
+ ))
+ ) : (
+
暂无榜单数据
+ )}
+
+
+
+
说明
+
+ 该榜单来自精选图库实时统计,可用于运营激励展示。
+
+
+
+
+
+
+
+
+ 下一步
+
+
+ 继续接支付与订阅
+
+
+ 查看新闻中心
+
+
+ 去后台运营配置
+
+
+
+
+
+ );
+}
diff --git a/app/lovart/page.tsx b/app/lovart/page.tsx
index 172ee51..f69cb19 100644
--- a/app/lovart/page.tsx
+++ b/app/lovart/page.tsx
@@ -1,6 +1,7 @@
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 {
@@ -109,28 +110,14 @@ export default async function LovartHomePage() {
-
- {featuredImages.slice(0, 4).map((image, index) => (
-
-
-
-
{image.prompt || "AI 品牌视觉草图"}
-
{image.modelId || "AI Model"}
-
-
- ))}
-
+ ({
+ id: image.id,
+ imageUrl: image.thumbnailUrl || image.originalUrl,
+ modelId: image.modelId || "AI Model",
+ prompt: image.prompt || "AI 品牌视觉草图",
+ }))}
+ />
diff --git a/components/lovart/hover-stack.tsx b/components/lovart/hover-stack.tsx
new file mode 100644
index 0000000..55b6d13
--- /dev/null
+++ b/components/lovart/hover-stack.tsx
@@ -0,0 +1,77 @@
+"use client";
+
+import { motion, useMotionValue, useSpring, useTransform } from "framer-motion";
+
+type StackItem = {
+ id: string;
+ imageUrl: string;
+ prompt: string;
+ modelId: string;
+};
+
+export function HoverStack({ items }: { items: StackItem[] }) {
+ const offsetX = useMotionValue(0);
+ const offsetY = useMotionValue(0);
+
+ const smoothX = useSpring(offsetX, { stiffness: 180, damping: 20, mass: 0.5 });
+ const smoothY = useSpring(offsetY, { stiffness: 180, damping: 20, mass: 0.5 });
+
+ const rotateY = useTransform(smoothX, [-160, 160], [-8, 8]);
+ const rotateX = useTransform(smoothY, [-120, 120], [7, -7]);
+
+ const cards = items.slice(0, 3);
+
+ if (cards.length === 0) {
+ return (
+
+ 暂无可展示样例
+
+ );
+ }
+
+ return (
+ {
+ offsetX.set(0);
+ offsetY.set(0);
+ }}
+ onMouseMove={(event) => {
+ const rect = event.currentTarget.getBoundingClientRect();
+ const x = event.clientX - (rect.left + rect.width / 2);
+ const y = event.clientY - (rect.top + rect.height / 2);
+ offsetX.set(x);
+ offsetY.set(y);
+ }}
+ style={{ rotateX, rotateY, transformStyle: "preserve-3d" }}
+ >
+ {cards.map((item, index) => {
+ const offset = index * 18;
+
+ return (
+
+
+
+
{item.prompt}
+
{item.modelId}
+
+
+ );
+ })}
+
+ );
+}
diff --git a/components/lovart/marketing-shell.tsx b/components/lovart/marketing-shell.tsx
index 53d64de..50fbcc2 100644
--- a/components/lovart/marketing-shell.tsx
+++ b/components/lovart/marketing-shell.tsx
@@ -17,6 +17,8 @@ const mainTabs = [
{ key: "home", label: "首页", href: "/lovart" },
{ key: "pricing", label: "定价", href: "/lovart/pricing" },
{ key: "news", label: "新闻", href: "/lovart/news" },
+ { key: "blog", label: "博客", href: "/lovart/blog" },
+ { key: "changelog", label: "更新日志", href: "/lovart/changelog" },
] as const;
const socialLinks = [
@@ -37,6 +39,10 @@ function resolveDocsLink(navLinks: NavLinkItem[], keywords: string[]) {
);
}
+function isExternal(url: string) {
+ return /^https?:\/\//i.test(url);
+}
+
export function MarketingShell({
siteName,
siteLogo,
@@ -49,14 +55,14 @@ export function MarketingShell({
siteName: string;
siteLogo?: string | null;
navLinks: NavLinkItem[];
- active: "home" | "pricing" | "news";
+ active: "home" | "pricing" | "news" | "blog" | "changelog";
bannerText?: string | null;
stats?: StatItem[];
children: ReactNode;
}) {
const docsUrl = resolveDocsLink(navLinks, ["文档", "docs"]);
- const blogUrl = resolveDocsLink(navLinks, ["博客", "blog"]);
- const changelogUrl = resolveDocsLink(navLinks, ["更新", "changelog", "release"]);
+ const blogUrl = resolveDocsLink(navLinks, ["博客", "blog"]) || "/lovart/blog";
+ const changelogUrl = resolveDocsLink(navLinks, ["更新", "changelog", "release"]) || "/lovart/changelog";
return (
@@ -85,29 +91,52 @@ export function MarketingShell({