diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 30549e4..6892087 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -29,6 +29,9 @@ export default function RootLayout({ children: React.ReactNode; }) { const siteName = process.env.SITE_NAME || 'MoonTV'; + const announcement = + process.env.ANNOUNCEMENT || + '本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。'; return ( @@ -41,9 +44,9 @@ export default function RootLayout({ enableSystem disableTransitionOnChange > - - {children} - + + {children} + diff --git a/src/app/page.tsx b/src/app/page.tsx index c0c9137..df7d01d 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -13,6 +13,7 @@ import ContinueWatching from '@/components/ContinueWatching'; import DemoCard from '@/components/DemoCard'; import PageLayout from '@/components/PageLayout'; import ScrollableRow from '@/components/ScrollableRow'; +import { useSite } from '@/components/SiteProvider'; import VideoCard from '@/components/VideoCard'; function HomeClient() { @@ -20,6 +21,16 @@ function HomeClient() { const [hotMovies, setHotMovies] = useState([]); const [hotTvShows, setHotTvShows] = useState([]); const [loading, setLoading] = useState(true); + const { announcement } = useSite(); + + const [showAnnouncement, setShowAnnouncement] = useState(() => { + // 检查本地存储中是否已记录弹窗显示状态 + const hasSeenAnnouncement = localStorage.getItem('hasSeenAnnouncement'); + if (hasSeenAnnouncement !== announcement) { + return true; + } + return !hasSeenAnnouncement && announcement; // 未记录且有公告时显示弹窗 + }); // 收藏夹数据 type FavoriteItem = { @@ -88,6 +99,11 @@ function HomeClient() { })(); }, [activeTab]); + const handleCloseAnnouncement = (announcement: string) => { + setShowAnnouncement(false); + localStorage.setItem('hasSeenAnnouncement', announcement); // 记录已查看弹窗 + }; + return (
@@ -233,6 +249,40 @@ function HomeClient() { )}
+ {announcement && showAnnouncement && ( +
+
+
+

+ 提示 +

+ +
+
+
+
+

+ {announcement} +

+
+
+ +
+
+ )}
); } diff --git a/src/components/AuthProvider.tsx b/src/components/AuthProvider.tsx index 590ac9b..c972a2f 100644 --- a/src/components/AuthProvider.tsx +++ b/src/components/AuthProvider.tsx @@ -1,7 +1,11 @@ 'use client'; import { usePathname, useRouter } from 'next/navigation'; -import { useEffect } from 'react'; +import { useCallback, useEffect, useState } from 'react'; + +import { ThemeToggle } from '@/components/ThemeToggle'; + +import { useSite } from './SiteProvider'; interface Props { children: React.ReactNode; @@ -10,37 +14,66 @@ interface Props { export default function AuthProvider({ children }: Props) { const router = useRouter(); const pathname = usePathname(); + const [isAuthenticated, setIsAuthenticated] = useState(null); + const { siteName } = useSite(); + const authenticate = useCallback(async () => { + // 登录页 + if (pathname.startsWith('/login') || pathname.startsWith('/api')) { + setIsAuthenticated(true); + return; + } - useEffect(() => { - // 登录页或 API 路径不做校验,避免死循环 - if (pathname.startsWith('/login')) return; - + // 从localStorage获取密码 const password = localStorage.getItem('password'); const fullPath = typeof window !== 'undefined' ? window.location.pathname + window.location.search : pathname; - // 有密码时验证 - (async () => { - try { - const res = await fetch('/api/login', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ password }), - }); + // 尝试认证 + try { + const res = await fetch('/api/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ password }), + }); - if (!res.ok) { - // 校验未通过,清理并跳转登录 - localStorage.removeItem('password'); - router.replace(`/login?redirect=${encodeURIComponent(fullPath)}`); - } - } catch (error) { - // 网络错误等也认为未登录 - router.replace(`/login?redirect=${encodeURIComponent(fullPath)}`); - } - })(); + if (!res.ok) throw new Error('认证失败'); + + setIsAuthenticated(true); + } catch (error) { + // 认证失败,清理并跳转登录 + setIsAuthenticated(false); + localStorage.removeItem('password'); + router.replace(`/login?redirect=${encodeURIComponent(fullPath)}`); + } }, [pathname, router]); - return <>{children}; + useEffect(() => { + authenticate(); + }, [pathname, authenticate]); + + // 认证状态未知时显示加载状态 + if (!isAuthenticated) { + return ( +
+
+ +
+
+

+ {siteName} +

+
+
+
+

+ 正在验证您的身份,请稍候... +

+
+
+ ); + } else { + return <>{children}; + } } diff --git a/src/components/SiteProvider.tsx b/src/components/SiteProvider.tsx index 283b962..3223a31 100644 --- a/src/components/SiteProvider.tsx +++ b/src/components/SiteProvider.tsx @@ -2,8 +2,11 @@ import { createContext, ReactNode, useContext } from 'react'; -const SiteContext = createContext<{ siteName: string }>({ - siteName: 'MoonTV', // Default value +const SiteContext = createContext<{ siteName: string; announcement?: string }>({ + // 默认值 + siteName: 'MoonTV', + announcement: + '本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。', }); export const useSite = () => useContext(SiteContext); @@ -11,11 +14,15 @@ export const useSite = () => useContext(SiteContext); export function SiteProvider({ children, siteName, + announcement, }: { children: ReactNode; siteName: string; + announcement?: string; }) { return ( - {children} + + {children} + ); }