diff --git a/app/(admin)/settings/admin-settings-client.tsx b/app/(admin)/settings/admin-settings-client.tsx
index ce81cfce2..92bca7c8f 100644
--- a/app/(admin)/settings/admin-settings-client.tsx
+++ b/app/(admin)/settings/admin-settings-client.tsx
@@ -1160,6 +1160,7 @@ type SiteSettings = {
bannerEnabled: boolean;
bannerText: string;
bannerLink: string | null;
+ bannerActionText: string;
toolbox1Name: string;
toolbox1Enabled: boolean;
toolbox2Name: string;
@@ -2153,6 +2154,7 @@ export function AdminSettingsClient() {
bannerEnabled: true,
bannerText: "限时特惠:升级会员最高立享 45% OFF!",
bannerLink: null,
+ bannerActionText: "立即查看",
toolbox1Name: "工具箱",
toolbox1Enabled: true,
toolbox2Name: "工具箱2",
@@ -2273,6 +2275,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",
@@ -12254,7 +12257,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 0323829a5..36a5256f0 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 折优惠",
@@ -1470,6 +1480,7 @@ export async function GET(request: NextRequest) {
bannerEnabled: true,
bannerText: "限时特惠:升级会员最高立享 45% OFF!",
bannerLink: null,
+ bannerActionText: "立即查看",
toolbox1Name: "工具箱",
toolbox1Enabled: true,
toolbox2Name: "工具箱2",
@@ -1651,6 +1662,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(),
@@ -3111,6 +3123,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 b83343934..afa9f43fc 100644
--- a/components/home/lovart-home.tsx
+++ b/components/home/lovart-home.tsx
@@ -74,6 +74,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";
const ImageAnnotatorLazy = dynamic(
() => import("@/components/image-annotator").then((module) => module.ImageAnnotator),
@@ -447,6 +453,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"
@@ -625,6 +635,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[];
@@ -664,6 +678,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 =
@@ -676,6 +716,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"
@@ -1623,9 +1705,19 @@ export function LovartHome({
return (
+ {shouldShowHomeBanner ? (
+
+ ) : null}
window.scrollTo({ top: 0, behavior: "smooth" })}
showBrand={false}
siteName={siteName || "Lovart"}
@@ -1878,6 +1970,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}
@@ -1888,7 +1981,12 @@ export function LovartHome({
tone={effectiveResolvedTheme === "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 b8ae9e46a..e91d39be9 100644
--- a/components/home/lovart/lovart-sidebar.tsx
+++ b/components/home/lovart/lovart-sidebar.tsx
@@ -48,6 +48,7 @@ type LovartSidebarProps = {
toolbox1Links: NavLink[];
toolbox2Enabled: boolean;
toolbox2Links: NavLink[];
+ topOffset?: number;
tutorialEnabled: boolean;
tutorialVideos: TutorialVideo[];
videoCanvasEnabled: boolean;
@@ -76,6 +77,7 @@ export function LovartSidebar({
toolbox1Links,
toolbox2Enabled,
toolbox2Links,
+ topOffset = 0,
tutorialEnabled,
tutorialVideos,
videoCanvasEnabled,
@@ -167,6 +169,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 b904b48e9..1518f87e3 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-14 bg-white/90 backdrop-blur dark:bg-[#0a0a0c]/90",
className
)}
+ style={position === "fixed" ? { top: offsetTop } : undefined}
>