diff --git a/README.md b/README.md index 5d5b21e..29a0348 100644 --- a/README.md +++ b/README.md @@ -457,6 +457,7 @@ dockge/komodo 等 docker compose UI 也有自动更新功能 | SITE_BASE | 站点 url | 形如 https://example.com | 空 | | NEXT_PUBLIC_SITE_NAME | 站点名称 | 任意字符串 | MoonTV | | ANNOUNCEMENT | 站点公告 | 任意字符串 | 本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。 | +| ANNOUNCEMENT_DISPLAY_MODE | 公告显示模式 | once、every | once | | NEXT_PUBLIC_STORAGE_TYPE | 播放记录/收藏的存储方式 | redis、kvrocks、upstash、d1 | 无默认,必填字段 | | KVROCKS_URL | kvrocks 连接 url | 连接 url | 空 | | REDIS_URL | redis 连接 url | 连接 url | 空 | diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 99e5253..43e202c 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -357,6 +357,7 @@ interface StandaloneSourceScript { interface SiteConfig { SiteName: string; Announcement: string; + AnnouncementDisplayMode?: 'once' | 'every'; SearchDownstreamMaxPage: number; SiteInterfaceCacheTime: number; DoubanProxyType: string; @@ -10150,6 +10151,7 @@ const SiteConfigComponent = ({ const [siteSettings, setSiteSettings] = useState({ SiteName: '', Announcement: '', + AnnouncementDisplayMode: 'once', SearchDownstreamMaxPage: 1, SiteInterfaceCacheTime: 7200, DoubanProxyType: 'cmliussss-cdn-tencent', @@ -10304,6 +10306,10 @@ const SiteConfigComponent = ({ config.SiteConfig.MagnetAcgripReverseProxy || '', MagnetNyaaReverseProxy: config.SiteConfig.MagnetNyaaReverseProxy || '', EnableComments: config.SiteConfig.EnableComments || false, + AnnouncementDisplayMode: + config.SiteConfig.AnnouncementDisplayMode === 'every' + ? 'every' + : 'once', }); } }, [config]); @@ -10462,6 +10468,51 @@ const SiteConfigComponent = ({ /> + {/* 公告显示模式 */} +
+ +
+ + +
+
+ {/* 豆瓣数据源设置 */}
diff --git a/src/app/api/admin/site/route.ts b/src/app/api/admin/site/route.ts index 77a3a9e..30dd3f7 100644 --- a/src/app/api/admin/site/route.ts +++ b/src/app/api/admin/site/route.ts @@ -31,6 +31,7 @@ export async function POST(request: NextRequest) { const { SiteName, Announcement, + AnnouncementDisplayMode, SearchDownstreamMaxPage, SiteInterfaceCacheTime, DoubanProxyType, @@ -85,6 +86,7 @@ export async function POST(request: NextRequest) { } = body as { SiteName: string; Announcement: string; + AnnouncementDisplayMode?: 'once' | 'every'; SearchDownstreamMaxPage: number; SiteInterfaceCacheTime: number; DoubanProxyType: string; @@ -142,6 +144,9 @@ export async function POST(request: NextRequest) { if ( typeof SiteName !== 'string' || typeof Announcement !== 'string' || + (AnnouncementDisplayMode !== undefined && + AnnouncementDisplayMode !== 'once' && + AnnouncementDisplayMode !== 'every') || typeof SearchDownstreamMaxPage !== 'number' || typeof SiteInterfaceCacheTime !== 'number' || typeof DoubanProxyType !== 'string' || @@ -238,6 +243,7 @@ export async function POST(request: NextRequest) { adminConfig.SiteConfig = { SiteName, Announcement, + AnnouncementDisplayMode, SearchDownstreamMaxPage, SiteInterfaceCacheTime, DoubanProxyType, diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 3f8719a..2b5c92b 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -58,6 +58,9 @@ export default async function RootLayout({ let announcement = process.env.ANNOUNCEMENT || '本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。'; + // 公告显示模式:从环境变量读取,数据库模式下由管理面板配置覆盖 + let announcementDisplayMode: 'once' | 'every' = + process.env.ANNOUNCEMENT_DISPLAY_MODE === 'every' ? 'every' : 'once'; let doubanProxyType = process.env.NEXT_PUBLIC_DOUBAN_PROXY_TYPE || 'cmliussss-cdn-tencent'; @@ -136,6 +139,8 @@ export default async function RootLayout({ const config = await getConfig(); siteName = config.SiteConfig.SiteName; announcement = config.SiteConfig.Announcement; + announcementDisplayMode = + config.SiteConfig.AnnouncementDisplayMode === 'every' ? 'every' : 'once'; doubanProxyType = config.SiteConfig.DoubanProxyType; doubanProxy = config.SiteConfig.DoubanProxy; @@ -346,6 +351,7 @@ export default async function RootLayout({ diff --git a/src/app/page.tsx b/src/app/page.tsx index 349070e..831e21a 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -53,7 +53,7 @@ function HomeClient() { BangumiCalendarData[] >([]); const [loading, setLoading] = useState(true); - const { announcement } = useSite(); + const { announcement, announcementDisplayMode } = useSite(); // 首页模块配置状态 const [homeModules, setHomeModules] = useState([ { id: 'hotMovies', name: '热门电影', enabled: true, order: 0 }, @@ -362,6 +362,17 @@ function HomeClient() { // 检查公告弹窗状态 useEffect(() => { if (typeof window !== 'undefined' && announcement) { + // 会话级标记:只在首次访问站点时弹出,导航切回首页不重复弹 + if (sessionStorage.getItem('announcementShown')) { + return; + } + // 每次显示模式:每次新会话首次访问弹出一次 + if (announcementDisplayMode === 'every') { + setShowAnnouncement(true); + sessionStorage.setItem('announcementShown', '1'); + return; + } + // 单次显示模式:localStorage 记住已看过的公告文本,换公告则重新弹出 const hasSeenAnnouncement = localStorage.getItem('hasSeenAnnouncement'); if (hasSeenAnnouncement !== announcement) { setShowAnnouncement(true); @@ -369,7 +380,8 @@ function HomeClient() { setShowAnnouncement(Boolean(!hasSeenAnnouncement && announcement)); } } - }, [announcement]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [announcement, announcementDisplayMode]); useEffect(() => { const CACHE_DURATION = 60 * 60 * 1000; // 1小时 diff --git a/src/components/SiteProvider.tsx b/src/components/SiteProvider.tsx index 3a7a6df..53b53c0 100644 --- a/src/components/SiteProvider.tsx +++ b/src/components/SiteProvider.tsx @@ -5,12 +5,14 @@ import { createContext, ReactNode, useContext } from 'react'; const SiteContext = createContext<{ siteName: string; announcement?: string; + announcementDisplayMode?: 'once' | 'every'; tmdbApiKey?: string; }>({ // 默认值 siteName: 'MoonTVPlus', announcement: '本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。', + announcementDisplayMode: 'once', tmdbApiKey: '', }); @@ -20,15 +22,19 @@ export function SiteProvider({ children, siteName, announcement, + announcementDisplayMode, tmdbApiKey, }: { children: ReactNode; siteName: string; announcement?: string; + announcementDisplayMode?: 'once' | 'every'; tmdbApiKey?: string; }) { return ( - + {children} ); diff --git a/src/lib/admin.types.ts b/src/lib/admin.types.ts index 547b671..ba6de40 100644 --- a/src/lib/admin.types.ts +++ b/src/lib/admin.types.ts @@ -8,6 +8,8 @@ export interface AdminConfig { SiteConfig: { SiteName: string; Announcement: string; + // 公告显示模式:once=单次显示(每个用户每条公告仅显示一次,换公告则重新显示);every=每次显示(每次打开首页都显示) + AnnouncementDisplayMode?: 'once' | 'every'; SearchDownstreamMaxPage: number; SiteInterfaceCacheTime: number; DoubanProxyType: string; diff --git a/src/lib/config.ts b/src/lib/config.ts index f28e767..9305f80 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -265,6 +265,10 @@ async function getInitConfig( Announcement: process.env.ANNOUNCEMENT || '本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。', + AnnouncementDisplayMode: + process.env.ANNOUNCEMENT_DISPLAY_MODE === 'every' + ? 'every' + : 'once', SearchDownstreamMaxPage: Number(process.env.NEXT_PUBLIC_SEARCH_MAX_PAGE) || 5, SiteInterfaceCacheTime: cfgFile.cache_time || 7200, @@ -555,6 +559,11 @@ export function configSelfCheck(adminConfig: AdminConfig): AdminConfig { if (adminConfig.SiteConfig.EnableComments === undefined) { adminConfig.SiteConfig.EnableComments = false; } + // 确保公告显示模式存在 + if (adminConfig.SiteConfig.AnnouncementDisplayMode === undefined) { + adminConfig.SiteConfig.AnnouncementDisplayMode = + process.env.ANNOUNCEMENT_DISPLAY_MODE === 'every' ? 'every' : 'once'; + } if (adminConfig.SiteConfig.EnableRegistration === undefined) { adminConfig.SiteConfig.EnableRegistration = false; }