diff --git a/app/api/public/lovart/route.ts b/app/api/public/lovart/route.ts new file mode 100644 index 0000000..159d58d --- /dev/null +++ b/app/api/public/lovart/route.ts @@ -0,0 +1,96 @@ +import { type NextRequest, NextResponse } from "next/server"; +import { + getEnabledAiModelsWithProvider, + getNavLinks, + getPublicFeaturedImages, + getPublicFeaturedRanking, + getSiteSettings, + getTutorialVideos, +} from "@/lib/db/queries"; + +function formatProviderStats(models: Awaited>) { + const providerMap = new Map(); + + for (const item of models) { + const providerName = item.provider.name; + providerMap.set(providerName, (providerMap.get(providerName) ?? 0) + 1); + } + + return Array.from(providerMap.entries()).map(([name, count]) => ({ name, count })); +} + +export async function GET(request: NextRequest) { + try { + const view = request.nextUrl.searchParams.get("view") || "home"; + + const [settings, navLinks, models, videos] = await Promise.all([ + getSiteSettings(), + getNavLinks(), + getEnabledAiModelsWithProvider(), + getTutorialVideos(), + ]); + + if (view === "pricing") { + const featuredImages = await getPublicFeaturedImages(8); + + const response = NextResponse.json({ + view, + settings, + pricing: { + models: models.length, + tutorials: videos.length, + featuredSamples: featuredImages, + }, + }); + + response.headers.set("Cache-Control", "public, s-maxage=60, stale-while-revalidate=300"); + return response; + } + + if (view === "news") { + const [featuredImages, ranking] = await Promise.all([ + getPublicFeaturedImages(12), + settings.rankingEnabled ? getPublicFeaturedRanking(6) : Promise.resolve([]), + ]); + + const response = NextResponse.json({ + view, + settings, + news: { + videos, + featuredImages, + 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([]), + ]); + + const response = NextResponse.json({ + view: "home", + settings, + navLinks, + providers: formatProviderStats(models), + stats: { + modelCount: models.length, + tutorialCount: videos.length, + featuredCount: featuredImages.length, + }, + featuredImages, + ranking, + videos: settings.tutorialEnabled ? videos : [], + }); + + response.headers.set("Cache-Control", "public, s-maxage=60, stale-while-revalidate=300"); + return response; + } catch (error) { + console.error("[Public Lovart API] Error:", error); + return NextResponse.json({ error: "Failed to fetch Lovart public payload" }, { status: 500 }); + } +} diff --git a/app/lovart/layout.tsx b/app/lovart/layout.tsx new file mode 100644 index 0000000..67770b9 --- /dev/null +++ b/app/lovart/layout.tsx @@ -0,0 +1,15 @@ +import type { Metadata } from "next"; +import type { ReactNode } from "react"; + +export const metadata: Metadata = { + title: "Lovart Style Rebuild", + description: "Rebuilt marketing pages connected to current backend data.", +}; + +export default function LovartLayout({ + children, +}: { + children: ReactNode; +}) { + return children; +} diff --git a/app/lovart/news/page.tsx b/app/lovart/news/page.tsx new file mode 100644 index 0000000..0d311d7 --- /dev/null +++ b/app/lovart/news/page.tsx @@ -0,0 +1,192 @@ +import Link from "next/link"; +import { Calendar, Newspaper, PlayCircle, TrendingUp } from "lucide-react"; +import { MarketingShell } from "@/components/lovart/marketing-shell"; +import { Reveal } from "@/components/lovart/reveal"; +import { + getNavLinks, + getPublicFeaturedImages, + getPublicFeaturedRanking, + 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 LovartNewsPage() { + const [settings, navLinks, videos, featuredImages, ranking] = await Promise.all([ + getSiteSettings(), + getNavLinks(), + getTutorialVideos(), + getPublicFeaturedImages(12), + getPublicFeaturedRanking(6), + ]); + + const [headline, ...restVideos] = videos; + + return ( + + +
+

+ + News & Tutorials Feed +

+

Lovart 新闻中心

+

+ 页面内容实时读取后台教程管理与精选图流,运营发布后无需改代码即可同步到前台。 +

+
+
+ + +
+
+
+ + {formatDate(headline?.updatedAt || headline?.createdAt)} +
+

+ {headline?.title || "等待发布第一条动态"} +

+

+ {headline?.description || + "你可以在后台添加教程视频、标题和描述,前台会自动展示为新闻中心头条。"} +

+ + {headline ? ( + + + 观看头条内容 + + ) : null} +
+ +
+

精选更新流

+ {featuredImages.length > 0 ? ( +
+ {featuredImages.slice(0, 9).map((image) => ( + {image.prompt + ))} +
+ ) : ( +

暂无精选更新

+ )} +
+
+
+ + +
+

全部新闻

+
+ {restVideos.length > 0 ? ( + restVideos.map((video, index) => ( +
+
+ + + {formatDate(video.updatedAt || video.createdAt)} + + #{String(index + 1).padStart(2, "0")} +
+

{video.title}

+

{video.description || "暂无描述"}

+ + + 立即观看 + +
+ )) + ) : ( +
+ 暂无更多新闻内容,可在后台「教程设置」中继续发布。 +
+ )} +
+
+
+ + {settings.rankingEnabled ? ( + +
+
+ +

创作者热度榜

+
+
+ {ranking.length > 0 ? ( + ranking.map((item, index) => ( +
+ + {index + 1}. {item.userName || item.userEmail?.split("@")[0] || "未知用户"} + + {item.featuredCount} +
+ )) + ) : ( +

暂无榜单数据

+ )} +
+
+
+ ) : null} + + +
+

下一步开发建议

+
+ + 回到首页调样式 + + + 定价页联动支付 + + + 去后台更新内容 + +
+
+
+
+ ); +} diff --git a/app/lovart/page.tsx b/app/lovart/page.tsx new file mode 100644 index 0000000..172ee51 --- /dev/null +++ b/app/lovart/page.tsx @@ -0,0 +1,310 @@ +import Link from "next/link"; +import { ArrowRight, Bot, Crown, Film, Layers3, Sparkles, WandSparkles } from "lucide-react"; +import type { ReactNode } from "react"; +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()) + ); + + const featureStories = [ + { + title: "懂你所想,精准编辑", + desc: "上传参考图、文案和品牌设定,系统会自动串联模型能力并保持统一风格。", + icon: , + }, + { + title: "每一次创作,风格始终如一", + desc: "根据当前可用模型和工具箱规则自动生成工作流,不再手动重复搭建步骤。", + icon: , + }, + { + title: "设计参考,贵在精准", + desc: "精选图库与作者榜实时联动后台数据,运营改动后页面立即同步。", + icon: , + }, + ]; + + return ( + + +
+
+

+ + Lovart Rebuild v1 · 全链路联调 +

+ +

+ 以系统思维设计 + + {siteName} + +

+ +

+ 已连接站点配置、模型中心、精选图库、教程源与作者榜数据。 + 你可以直接把 `/lovart` 作为新的营销主页继续迭代细节。 +

+ +
+ + 立即体验 + + + + 查看定价方案 + +
+ +
+ } label="模型可用" value={String(models.length)} /> + } label="导航入口" value={String(navLinks.length)} /> + } label="教程总数" value={String(videos.length)} /> +
+
+ +
+ {featuredImages.slice(0, 4).map((image, index) => ( +
+ {image.prompt +
+

{image.prompt || "AI 品牌视觉草图"}

+

{image.modelId || "AI Model"}

+
+
+ ))} +
+
+
+ + +
+ {featureStories.map((item) => ( +
+ + {item.icon} + +

{item.title}

+

{item.desc}

+
+ ))} +
+
+ + +
+
+

模型能力分布

+
+ {providerStats.length > 0 ? ( + providerStats.map(([providerName, count]) => ( +
+
+ {providerName} + {count} +
+
+
+
+
+ )) + ) : ( +

暂无可用模型

+ )} +
+
+ +
+
+

精选画廊

+ + 查看全部 + +
+ + {featuredImages.length > 0 ? ( +
+ {featuredImages.slice(0, 9).map((image) => ( + {image.prompt + ))} +
+ ) : ( +

暂无精选数据

+ )} +
+
+
+ + {settings.rankingEnabled ? ( + +
+

精选作者榜

+
+ {ranking.length > 0 ? ( + ranking.map((item, index) => ( +
+
+ + {index + 1} + +
+

+ {item.userName || item.userEmail?.split("@")[0] || "未知用户"} +

+

{item.userDepartment || "未设置部门"}

+
+
+ {item.featuredCount} +
+ )) + ) : ( +

暂无榜单数据

+ )} +
+
+
+ ) : null} + + {settings.tutorialEnabled ? ( + +
+
+

最新教程内容

+ + 查看全部动态 + +
+ +
+ {videos.slice(0, 6).map((video) => ( + + ))} +
+
+
+ ) : null} + + +
+

用 Lovart 设计

+

+ 当前页面已完成核心数据联调,可继续进行 1:1 交互动效精修、国际化文案和支付系统接入。 +

+
+ + 免费开始 + + + 打开后台继续配置 + +
+
+
+
+ ); +} + +function MetricCard({ + icon, + label, + value, +}: { + icon: ReactNode; + label: string; + value: string; +}) { + return ( +
+

+ {icon} + {label} +

+

{value}

+
+ ); +} diff --git a/app/lovart/pricing/page.tsx b/app/lovart/pricing/page.tsx new file mode 100644 index 0000000..d7c8f89 --- /dev/null +++ b/app/lovart/pricing/page.tsx @@ -0,0 +1,228 @@ +import Link from "next/link"; +import { Check, Rocket, ShieldCheck } from "lucide-react"; +import { MarketingShell } from "@/components/lovart/marketing-shell"; +import { Reveal } from "@/components/lovart/reveal"; +import { + getEnabledAiModelsWithProvider, + getNavLinks, + getPublicFeaturedImages, + getSiteSettings, + getTutorialVideos, +} from "@/lib/db/queries"; + +export default async function LovartPricingPage() { + const [settings, navLinks, models, videos, featuredImages] = await Promise.all([ + getSiteSettings(), + getNavLinks(), + getEnabledAiModelsWithProvider(), + getTutorialVideos(), + getPublicFeaturedImages(8), + ]); + + const modelCount = models.length; + const tutorialCount = videos.length; + + const plans = [ + { + name: "Starter", + price: "¥0", + period: "/月", + desc: "适合快速体验 AI 设计流程", + features: [ + "基础文生图与简单编辑", + `最多 ${Math.max(3, Math.floor(modelCount / 3))} 个模型试用`, + "社区支持与基础教程", + ], + }, + { + name: "Basic", + price: "¥69", + period: "/月", + desc: "适合独立创作者日常出图", + features: ["高清导出", "多风格模板", "历史记录与作品管理"], + }, + { + name: "Pro", + price: "¥199", + period: "/月", + desc: "适合高频生产内容的个人与团队", + features: [ + `完整 ${modelCount} 个在线模型能力`, + `附赠 ${tutorialCount} 条教程与工作流示例`, + "优先队列与高级编辑功能", + ], + highlight: true, + }, + { + name: "Ultimate", + price: "¥599", + period: "/月", + desc: "适合企业协作与品牌运营", + features: ["团队权限管理", "批量任务与自动归档", "可定制品牌资产与 SSO 接入"], + }, + ]; + + const faqs = [ + { + q: "可以随时升级或降级套餐吗?", + a: "支持。套餐调整后会立刻刷新权限,已生成内容不会丢失。", + }, + { + q: "定价文案和能力是固定的吗?", + a: "不是。当前页面已与后台模型和教程配置联动,后续接入支付后会直接映射到真实权益。", + }, + { + q: "企业版是否支持私有部署?", + a: "支持按需评估,可在现有系统基础上扩展存储、鉴权和审计策略。", + }, + ]; + + return ( + + +
+

+ + Pricing Blueprint +

+

选择你的专属方案

+

+ 套餐文案已与后端模型、教程和精选样例联动。下一步接入支付接口后即可直接上线。 +

+
+
+ + +
+ {plans.map((plan) => ( +
+

{plan.name}

+
+ {plan.price} + {plan.period} +
+

{plan.desc}

+ +
    + {plan.features.map((feature) => ( +
  • + + {feature} +
  • + ))} +
+ + + 选择 {plan.name} + +
+ ))} +
+
+ + +
+
+

计费与能力说明

+
    +
  • + + 套餐可在后台自由调整模型与功能开关,页面会自动反映实际能力。 +
  • +
  • + + 当前在线模型:{modelCount},教程内容:{tutorialCount},用于驱动套餐文案。 +
  • +
  • + + 支持接入 Stripe / 微信支付,并保留当前视觉样式。 +
  • +
+
+ +
+

精选样例

+ {featuredImages.length > 0 ? ( +
+ {featuredImages.map((image) => ( + {image.prompt + ))} +
+ ) : ( +

暂无精选图片

+ )} +
+
+
+ + +
+

常见问题

+
+ {faqs.map((item) => ( +
+ + {item.q} + +

{item.a}

+
+ ))} +
+
+
+ + +
+

用 Lovart 设计

+

+ 目前已完成高保真定价页重建和后端联调,下一步建议直接接入订阅支付与权益同步。 +

+
+ + 开始免费试用 + + + 去后台调整套餐文案 + +
+
+
+
+ ); +} diff --git a/components/lovart/marketing-shell.tsx b/components/lovart/marketing-shell.tsx new file mode 100644 index 0000000..53d64de --- /dev/null +++ b/components/lovart/marketing-shell.tsx @@ -0,0 +1,201 @@ +import Link from "next/link"; +import { ArrowUpRight, Sparkles } from "lucide-react"; +import type { ReactNode } from "react"; + +type NavLinkItem = { + id: string; + name: string; + url: string; +}; + +type StatItem = { + label: string; + value: string; +}; + +const mainTabs = [ + { key: "home", label: "首页", href: "/lovart" }, + { key: "pricing", label: "定价", href: "/lovart/pricing" }, + { key: "news", label: "新闻", href: "/lovart/news" }, +] as const; + +const socialLinks = [ + { label: "Discord", href: "https://discord.com/invite/3Fr4KPrwbA" }, + { label: "X", href: "https://twitter.com/lovart_ai" }, + { label: "YouTube", href: "https://www.youtube.com/@lovart_ai" }, + { label: "LinkedIn", href: "https://www.linkedin.com/company/lovart-ai" }, + { label: "Instagram", href: "https://www.instagram.com/lovart.ai" }, + { label: "TikTok", href: "https://www.tiktok.com/@lovart_ai" }, +]; + +function resolveDocsLink(navLinks: NavLinkItem[], keywords: string[]) { + const loweredKeywords = keywords.map((keyword) => keyword.toLowerCase()); + return ( + navLinks.find((link) => + loweredKeywords.some((keyword) => link.name.toLowerCase().includes(keyword)) + )?.url ?? null + ); +} + +export function MarketingShell({ + siteName, + siteLogo, + navLinks, + active, + bannerText, + stats, + children, +}: { + siteName: string; + siteLogo?: string | null; + navLinks: NavLinkItem[]; + active: "home" | "pricing" | "news"; + bannerText?: string | null; + stats?: StatItem[]; + children: ReactNode; +}) { + const docsUrl = resolveDocsLink(navLinks, ["文档", "docs"]); + const blogUrl = resolveDocsLink(navLinks, ["博客", "blog"]); + const changelogUrl = resolveDocsLink(navLinks, ["更新", "changelog", "release"]); + + return ( +
+
+
+ +
+ {bannerText ? ( +
+ {bannerText} +
+ ) : null} + +
+ + {siteLogo ? ( + {siteName} + ) : ( + + + + )} + + {siteName} + + + + + +
+ + 登录 + + + 免费开始 + +
+
+ + {stats && stats.length > 0 ? ( +
+
+ {stats.map((item) => ( + + {item.label} + {item.value} + + ))} +
+
+ ) : null} +
+ +
{children}
+ +
+ {mainTabs.map((tab) => ( + + {tab.label} + + ))} +
+ + +
+ ); +} diff --git a/components/lovart/reveal.tsx b/components/lovart/reveal.tsx new file mode 100644 index 0000000..1877167 --- /dev/null +++ b/components/lovart/reveal.tsx @@ -0,0 +1,28 @@ +"use client"; + +import { motion } from "framer-motion"; +import type { ReactNode } from "react"; + +export function Reveal({ + children, + delay = 0, + y = 22, + className, +}: { + children: ReactNode; + delay?: number; + y?: number; + className?: string; +}) { + return ( + + {children} + + ); +} diff --git a/docs/lovart-重建接口联调清单.md b/docs/lovart-重建接口联调清单.md new file mode 100644 index 0000000..80058ff --- /dev/null +++ b/docs/lovart-重建接口联调清单.md @@ -0,0 +1,57 @@ +# Lovart 重建接口联调清单 + +更新时间:2026-02-12 + +## 已接入(当前可用) + +| 页面 | 数据来源 | 方式 | 状态 | +|---|---|---|---| +| `/lovart` | `getSiteSettings` | DB 直连(RSC) | 已接入 | +| `/lovart` | `getNavLinks` | DB 直连(RSC) | 已接入 | +| `/lovart` | `getEnabledAiModelsWithProvider` | DB 直连(RSC) | 已接入 | +| `/lovart` | `getTutorialVideos` | DB 直连(RSC) | 已接入 | +| `/lovart` | `getPublicFeaturedImages` | DB 直连(RSC) | 已接入 | +| `/lovart` | `getPublicFeaturedRanking` | DB 直连(RSC) | 已接入 | +| `/lovart/pricing` | 模型、教程、精选样例 | DB 直连(RSC) | 已接入 | +| `/lovart/news` | 教程流 + 精选流 + 榜单 | DB 直连(RSC) | 已接入 | +| `/lovart*` 公开访问 | `proxy.ts` 放行 | 中间件路由白名单 | 已接入 | +| `/api/public/lovart?view=home` | 首页聚合数据 | Public API | 已接入 | +| `/api/public/lovart?view=pricing` | 定价页聚合数据 | Public API | 已接入 | +| `/api/public/lovart?view=news` | 新闻页聚合数据 | Public API | 已接入 | + +## 新增查询与 API + +- 查询函数(`lib/db/queries.ts`) + - `getPublicFeaturedImages(limit)` + - `getPublicFeaturedRanking(limit)` + - `getTutorialVideos()`(改为静态导入,减少运行时开销) +- 公开接口(`app/api/public/lovart/route.ts`) + - `GET /api/public/lovart?view=home|pricing|news` + - 返回公共营销页所需聚合数据(含短缓存头) + +## 页面层联动点 + +| 页面 | 联动项 | 说明 | +|---|---|---| +| `/lovart` | `siteSettings.bannerEnabled/bannerText` | 顶部公告条同步后台配置 | +| `/lovart` | `siteSettings.rankingEnabled` | 控制是否显示作者榜 | +| `/lovart` | `siteSettings.tutorialEnabled` | 控制是否展示教程区块 | +| `/lovart` | `siteSettings.toolbox1Name/toolbox2Name` + `navLinks.toolboxGroup` | 统计工具箱入口并展示在顶部指标条 | +| `/lovart/pricing` | 模型/教程/精选数量 | 套餐文案与样例数量实时同步 | +| `/lovart/news` | 视频更新时间 + 精选流 + 榜单 | 新闻头条和列表动态生成 | + +## 暂未接入(后续) + +| 项目 | 说明 | 后续动作 | +|---|---|---| +| 转化事件埋点 | 参考站有较重埋点(GTM/像素) | 接入统一埋点层,避免散落脚本 | +| 支付与订阅 | 当前仅展示定价 UI | 接 Stripe/微信支付并写回用户权益 | +| 多语言内容 | 参考站多语言切换 | 增加 i18n 路由和文案资源 | +| 复杂交互动效 | 目前是首版时序动效 | 按参考页面做逐区块微交互调优 | +| 页面 AB 参数化 | 参考站含实验参数 | 结合后台配置做模块开关 | + +## 风险与注意 + +- 当前公开页直接走服务端查询,性能可控;高并发建议在查询层增加缓存。 +- 新增 Public API 仅输出营销页公共数据,不含私密用户字段。 +- 若后续改成纯客户端渲染,可直接消费 `/api/public/lovart`,减少页面层重复聚合逻辑。 diff --git a/lib/db/queries.ts b/lib/db/queries.ts index e0524f9..eb00410 100644 --- a/lib/db/queries.ts +++ b/lib/db/queries.ts @@ -49,6 +49,7 @@ import { authSettings, loginToken, generatedImage, + tutorialVideos, } from "./schema"; import { generateHashedPassword } from "./utils"; @@ -1638,18 +1639,57 @@ export async function checkUserImageQuota(userId: string): Promise<{ } } +export async function getPublicFeaturedImages(limit = 12) { + try { + return await db + .select({ + id: generatedImage.id, + thumbnailUrl: generatedImage.thumbnailUrl, + originalUrl: generatedImage.originalUrl, + prompt: generatedImage.prompt, + imageSize: generatedImage.imageSize, + modelId: generatedImage.modelId, + createdAt: generatedImage.createdAt, + }) + .from(generatedImage) + .where(eq(generatedImage.isFeatured, true)) + .orderBy(desc(generatedImage.featuredAt), desc(generatedImage.createdAt)) + .limit(limit); + } catch (_error) { + return []; + } +} + +export async function getPublicFeaturedRanking(limit = 5) { + try { + return await db + .select({ + userId: generatedImage.userId, + userName: user.name, + userEmail: user.email, + userDepartment: user.department, + featuredCount: sql`count(*)::int`, + }) + .from(generatedImage) + .leftJoin(user, eq(generatedImage.userId, user.id)) + .where(eq(generatedImage.isFeatured, true)) + .groupBy(generatedImage.userId, user.name, user.email, user.department) + .orderBy(desc(sql`count(*)`)) + .limit(limit); + } catch (_error) { + return []; + } +} + // 获取视频教程列表 export async function getTutorialVideos() { try { - const { tutorialVideos } = await import("./schema"); - const { eq, asc } = await import("drizzle-orm"); - const videos = await db .select() .from(tutorialVideos) .where(eq(tutorialVideos.isEnabled, true)) .orderBy(asc(tutorialVideos.order)); - + return videos; } catch (_error) { return []; diff --git a/proxy.ts b/proxy.ts index c2333e5..542a03a 100644 --- a/proxy.ts +++ b/proxy.ts @@ -6,6 +6,12 @@ import { isSuspiciousRequest, logSuspiciousRequest } from "@/lib/security/reques export async function proxy(request: NextRequest) { const { pathname } = request.nextUrl; + const isPublicMarketingRoute = + pathname === "/lovart" || + pathname.startsWith("/lovart/") || + pathname === "/lovart/pricing" || + pathname === "/lovart/news"; + const isPublicMarketingApi = pathname === "/api/public/lovart" || pathname.startsWith("/api/public/lovart/"); // 🛡️ 安全检查1:阻止黑名单IP const clientIP = getClientIP(request); @@ -69,6 +75,11 @@ export async function proxy(request: NextRequest) { return NextResponse.next(); } + // 允许公开营销数据 API(Lovart 重建页) + if (isPublicMarketingApi) { + return NextResponse.next(); + } + // 允许访问钉钉回调页面(public 文件夹中的文件) if (pathname === "/dingtalk-callback.html") { return NextResponse.next(); @@ -89,7 +100,12 @@ export async function proxy(request: NextRequest) { `${request.nextUrl.protocol}//${request.headers.get("host") ?? request.nextUrl.host}`; // 未登录用户只能访问 /login 和 /register - if (!token && !["/login", "/register"].includes(pathname)) { + if ( + !token && + !["/login", "/register"].includes(pathname) && + !isPublicMarketingRoute && + !isPublicMarketingApi + ) { // 对于 API 路由的非 GET 请求,直接返回 401 JSON 响应(不重定向) if (pathname.startsWith("/api/") && request.method !== "GET") { return NextResponse.json(