diff --git a/app/(admin)/settings/admin-settings-client.tsx b/app/(admin)/settings/admin-settings-client.tsx
index dba9be50b..d4e380bf0 100644
--- a/app/(admin)/settings/admin-settings-client.tsx
+++ b/app/(admin)/settings/admin-settings-client.tsx
@@ -1164,6 +1164,7 @@ type SiteSettings = {
bannerEnabled: boolean;
bannerText: string;
bannerLink: string | null;
+ bannerActionText: string;
toolbox1Name: string;
toolbox1Enabled: boolean;
toolbox2Name: string;
@@ -2150,6 +2151,7 @@ export function AdminSettingsClient() {
bannerEnabled: true,
bannerText: "限时特惠:升级会员最高立享 45% OFF!",
bannerLink: null,
+ bannerActionText: "立即查看",
toolbox1Name: "工具箱",
toolbox1Enabled: true,
toolbox2Name: "工具箱2",
@@ -2270,6 +2272,7 @@ export function AdminSettingsClient() {
bannerText:
siteSettingsData.bannerText || "限时特惠:升级会员最高立享 45% OFF!",
bannerLink: siteSettingsData.bannerLink || null,
+ bannerActionText: siteSettingsData.bannerActionText || "立即查看",
toolbox1Name: siteSettingsData.toolbox1Name || "工具箱",
toolbox1Enabled: siteSettingsData.toolbox1Enabled ?? true,
toolbox2Name: siteSettingsData.toolbox2Name || "工具箱2",
@@ -12327,7 +12330,7 @@ export function AdminSettingsClient() {
- 在无限画布等页面的聊天面板中显示广告横幅
+ 在首页顶部和无限画布聊天面板中显示广告横幅
+
+
+
+ setSiteSettings({
+ ...siteSettings,
+ bannerActionText: e.target.value,
+ })
+ }
+ placeholder="例如:立即升级"
+ value={siteSettings.bannerActionText}
+ />
+
>
diff --git a/app/api/site-settings/route.ts b/app/api/site-settings/route.ts
index 8720029e7..a6db2a86b 100644
--- a/app/api/site-settings/route.ts
+++ b/app/api/site-settings/route.ts
@@ -579,6 +579,10 @@ const HOME_SITE_SETTINGS_FALLBACK: Record = {
favicon: null,
lovartPromoText: "现在升级,立享 Kling 3.0 与 Nano Banana Pro,最高 5 折优惠",
lovartPromoActionText: "立即升级",
+ bannerEnabled: true,
+ bannerText: "限时特惠:升级会员最高立享 45% OFF!",
+ bannerLink: null,
+ bannerActionText: "立即查看",
lovartHeroTitle: "由壹点AI 让设计更简单",
lovartHeroSubtitle: "懂你的设计代理,帮你搞定一切",
surpriseCategories: ["全部", "精选"],
@@ -897,6 +901,7 @@ async function getScopedPublicSiteSettings(
bannerText:
typedSettings?.bannerText || "限时特惠:升级会员最高立享 45% OFF!",
bannerLink: typedSettings?.bannerLink || null,
+ bannerActionText: typedSettings?.bannerActionText || "立即查看",
lovartComposerPrefix: normalizeLovartComposerPrefix(
valueByKey.get(LOVART_COMPOSER_PREFIX_KEY)
),
@@ -939,6 +944,11 @@ async function getScopedPublicSiteSettings(
return createScopedSiteSettingsResponse(scope, {
...createBrandingSettings(typedSettings),
+ bannerEnabled: typedSettings?.bannerEnabled ?? true,
+ bannerText:
+ typedSettings?.bannerText || "限时特惠:升级会员最高立享 45% OFF!",
+ bannerLink: typedSettings?.bannerLink || null,
+ bannerActionText: typedSettings?.bannerActionText || "立即查看",
lovartPromoText:
typedSettings?.lovartPromoText ||
"现在升级,立享 Kling 3.0 与 Nano Banana Pro,最高 5 折优惠",
@@ -1444,6 +1454,7 @@ export async function GET(request: NextRequest) {
bannerEnabled: true,
bannerText: "限时特惠:升级会员最高立享 45% OFF!",
bannerLink: null,
+ bannerActionText: "立即查看",
toolbox1Name: "工具箱",
toolbox1Enabled: true,
toolbox2Name: "工具箱2",
@@ -1625,6 +1636,7 @@ const updateSchema = z.object({
bannerEnabled: z.boolean().optional(),
bannerText: z.string().optional(),
bannerLink: z.string().nullable().optional(),
+ bannerActionText: z.string().max(32).optional(),
toolbox1Name: z.string().max(64).optional(),
toolbox1Enabled: z.boolean().optional(),
toolbox2Name: z.string().max(64).optional(),
@@ -3085,6 +3097,7 @@ export async function PUT(request: NextRequest) {
bannerEnabled: siteSettingsPayload.bannerEnabled ?? true,
bannerText: siteSettingsPayload.bannerText ?? "限时特惠:升级会员最高立享 45% OFF!",
bannerLink: siteSettingsPayload.bannerLink,
+ bannerActionText: siteSettingsPayload.bannerActionText ?? "立即查看",
toolbox1Name: siteSettingsPayload.toolbox1Name ?? "工具箱",
toolbox1Enabled: siteSettingsPayload.toolbox1Enabled ?? true,
toolbox2Name: siteSettingsPayload.toolbox2Name ?? "工具箱2",
diff --git a/components/home/lovart-home.tsx b/components/home/lovart-home.tsx
index 6f9928930..d29c05920 100644
--- a/components/home/lovart-home.tsx
+++ b/components/home/lovart-home.tsx
@@ -70,6 +70,12 @@ import { LovartFeaturedGallery } from "@/components/home/lovart/lovart-featured-
import { LovartHeroComposer } from "@/components/home/lovart/lovart-hero-composer";
import { LovartProjectsSection } from "@/components/home/lovart/lovart-projects-section";
import { LovartSidebar } from "@/components/home/lovart/lovart-sidebar";
+import {
+ LOVART_PROMO_BANNER_HEIGHT,
+ LovartPromoBanner,
+} from "@/components/home/lovart/lovart-promo-banner";
+
+const HOME_PROMO_DISMISS_STORAGE_KEY = "lovart-home-promo-dismissed-v1";
type ImageAnnotatorComponent = typeof import("@/components/image-annotator").ImageAnnotator;
@@ -431,6 +437,10 @@ export function LovartHome({
const [activeCategory, setActiveCategory] = useState("全部");
const [isProjectsOnlyView, setIsProjectsOnlyView] = useState(false);
const [upgradeDialogOpen, setUpgradeDialogOpen] = useState(false);
+ const [dismissedHomeBannerIdentity, setDismissedHomeBannerIdentity] =
+ useState(null);
+ const [hasResolvedHomeBannerDismissal, setHasResolvedHomeBannerDismissal] =
+ useState(false);
const [accountCenterOpen, setAccountCenterOpen] = useState(false);
const [accountCenterTab, setAccountCenterTab] = useState<"account" | "usage" | "billing">(
"account"
@@ -604,6 +614,10 @@ export function LovartHome({
siteLogoDark?: string | null;
lovartPromoText?: string | null;
lovartPromoActionText?: string | null;
+ bannerEnabled?: boolean;
+ bannerText?: string | null;
+ bannerLink?: string | null;
+ bannerActionText?: string | null;
lovartHeroTitle?: string | null;
lovartHeroSubtitle?: string | null;
surpriseCategories?: string[];
@@ -643,6 +657,32 @@ export function LovartHome({
siteSettingsData?.lovartPromoText ?? initialHeroPromoText;
const heroPromoActionText =
siteSettingsData?.lovartPromoActionText ?? initialHeroPromoActionText;
+ const homeBannerText = String(siteSettingsData?.bannerText || "").trim();
+ const homeBannerActionText = String(
+ siteSettingsData?.bannerActionText || "立即查看"
+ ).trim();
+ const rawHomeBannerLink = String(siteSettingsData?.bannerLink || "").trim();
+ const homeBannerLink =
+ /^(https?:\/\/|\/(?!\/))/i.test(rawHomeBannerLink)
+ ? rawHomeBannerLink
+ : "";
+ const homeBannerIdentity = useMemo(
+ () =>
+ homeBannerText
+ ? JSON.stringify({
+ actionText: homeBannerActionText,
+ link: homeBannerLink,
+ text: homeBannerText,
+ })
+ : "",
+ [homeBannerActionText, homeBannerLink, homeBannerText]
+ );
+ const shouldShowHomeBanner = Boolean(
+ siteSettingsData?.bannerEnabled === true &&
+ homeBannerIdentity &&
+ hasResolvedHomeBannerDismissal &&
+ dismissedHomeBannerIdentity !== homeBannerIdentity
+ );
const heroTitleText =
siteSettingsData?.lovartHeroTitle ?? initialHeroTitleText;
const heroSubtitleText =
@@ -654,6 +694,48 @@ export function LovartHome({
? siteLogoDark || siteLogo || ""
: siteLogo || siteLogoDark || "";
+ useEffect(() => {
+ if (!homeBannerIdentity) {
+ setDismissedHomeBannerIdentity(null);
+ setHasResolvedHomeBannerDismissal(true);
+ return;
+ }
+
+ try {
+ const storedIdentity = window.localStorage.getItem(
+ HOME_PROMO_DISMISS_STORAGE_KEY
+ );
+ setDismissedHomeBannerIdentity(
+ storedIdentity === homeBannerIdentity ? homeBannerIdentity : null
+ );
+ } catch {
+ setDismissedHomeBannerIdentity(null);
+ }
+ setHasResolvedHomeBannerDismissal(true);
+ }, [homeBannerIdentity]);
+
+ const dismissHomeBanner = useCallback(() => {
+ if (!homeBannerIdentity) return;
+ try {
+ window.localStorage.setItem(
+ HOME_PROMO_DISMISS_STORAGE_KEY,
+ homeBannerIdentity
+ );
+ } catch {
+ // The banner still closes for this page when storage is unavailable.
+ }
+ setDismissedHomeBannerIdentity(homeBannerIdentity);
+ }, [homeBannerIdentity]);
+
+ const openHomeBannerLink = useCallback(() => {
+ if (!homeBannerLink) return;
+ if (homeBannerLink.startsWith("/")) {
+ router.push(homeBannerLink);
+ return;
+ }
+ window.open(homeBannerLink, "_blank", "noopener,noreferrer");
+ }, [homeBannerLink, router]);
+
const { data: tutorialVideosData } = useSWR<{ videos: TutorialVideo[] }>(
isSecondaryDataReady && siteSettingsData?.tutorialEnabled
? "/api/tutorial-videos"
@@ -1522,9 +1604,19 @@ export function LovartHome({
return (
+ {shouldShowHomeBanner ? (
+
+ ) : null}
window.scrollTo({ top: 0, behavior: "smooth" })}
showBrand={false}
siteName={siteName || "Lovart"}
@@ -1779,6 +1871,7 @@ export function LovartHome({
}
siteLogo={displayLogo}
siteName={siteName || "Lovart"}
+ topOffset={shouldShowHomeBanner ? LOVART_PROMO_BANNER_HEIGHT : 0}
toolbox1Enabled={siteSettingsData?.toolbox1Enabled !== false}
toolbox1Links={toolbox1Links}
toolbox2Enabled={siteSettingsData?.toolbox2Enabled === true}
@@ -1789,7 +1882,12 @@ export function LovartHome({
tone={resolvedTheme === "dark" ? "dark" : "light"}
/>
-
+
{!isProjectsOnlyView && (
void;
+ onDismiss: () => void;
+ text: string;
+};
+
+export const LOVART_PROMO_BANNER_HEIGHT = 45;
+
+export function LovartPromoBanner({
+ actionText,
+ link,
+ onAction,
+ onDismiss,
+ text,
+}: LovartPromoBannerProps) {
+ const showAction = Boolean(link?.trim() && actionText?.trim());
+
+ return (
+
+
+
+
+ {text}
+
+ {showAction ? (
+
+ ) : null}
+
+
+
+ );
+}
diff --git a/components/home/lovart/lovart-sidebar.tsx b/components/home/lovart/lovart-sidebar.tsx
index 6b76a41bb..d9f817e59 100644
--- a/components/home/lovart/lovart-sidebar.tsx
+++ b/components/home/lovart/lovart-sidebar.tsx
@@ -51,6 +51,7 @@ type LovartSidebarProps = {
toolbox1Links: NavLink[];
toolbox2Enabled: boolean;
toolbox2Links: NavLink[];
+ topOffset?: number;
tutorialEnabled: boolean;
tutorialVideos: TutorialVideo[];
videoCanvasEnabled: boolean;
@@ -81,6 +82,7 @@ export function LovartSidebar({
toolbox1Links,
toolbox2Enabled,
toolbox2Links,
+ topOffset = 0,
tutorialEnabled,
tutorialVideos,
videoCanvasEnabled,
@@ -180,6 +182,7 @@ export function LovartSidebar({
? "border-white/10 bg-black"
: "border-[#e9e9e9] bg-white",
].join(" ")}
+ style={{ top: topOffset }}
>
diff --git a/components/layout/brand-top-bar.tsx b/components/layout/brand-top-bar.tsx
index 32af236b0..6d369f7b1 100644
--- a/components/layout/brand-top-bar.tsx
+++ b/components/layout/brand-top-bar.tsx
@@ -10,6 +10,7 @@ type BrandTopBarProps = {
className?: string;
contentClassName?: string;
logo?: string | null;
+ offsetTop?: number;
onLogoClick?: () => void;
position?: "fixed" | "sticky" | "static";
showBrand?: boolean;
@@ -22,6 +23,7 @@ export function BrandTopBar({
className,
contentClassName,
logo,
+ offsetTop = 0,
onLogoClick,
position = "fixed",
showBrand = true,
@@ -42,6 +44,7 @@ export function BrandTopBar({
"h-[68px] bg-white/90 backdrop-blur dark:bg-[#0a0a0c]/90",
className
)}
+ style={position === "fixed" ? { top: offsetTop } : undefined}
>