From 4fac5e6409a3e7304adad5802d44a4c5d5cdcc23 Mon Sep 17 00:00:00 2001 From: SongPro Date: Tue, 1 Jul 2025 14:18:21 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat(AuthProvider):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=AE=A4=E8=AF=81=E7=8A=B6=E6=80=81=E7=AE=A1=E7=90=86=E5=92=8C?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD=E7=95=8C=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构认证逻辑为独立函数并添加状态管理,在认证过程中显示加载界面。同时添加主题切换按钮到加载界面。 --- src/components/AuthProvider.tsx | 88 ++++++++++++++++++++++++--------- 1 file changed, 64 insertions(+), 24 deletions(-) diff --git a/src/components/AuthProvider.tsx b/src/components/AuthProvider.tsx index 590ac9b..381e276 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,73 @@ 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/login')) { + 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 }), - }); + // 没有密码直接跳转 + if (!password) { + router.replace(`/login?redirect=${encodeURIComponent(fullPath)}`); + setIsAuthenticated(false); + return; + } - if (!res.ok) { - // 校验未通过,清理并跳转登录 - localStorage.removeItem('password'); - router.replace(`/login?redirect=${encodeURIComponent(fullPath)}`); - } - } catch (error) { - // 网络错误等也认为未登录 - router.replace(`/login?redirect=${encodeURIComponent(fullPath)}`); - } - })(); + // 尝试认证 + try { + const res = await fetch('/api/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ password }), + }); + + 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}; + } } From b045967f9c979b846619296c0d0dae451ed39991 Mon Sep 17 00:00:00 2001 From: SongPro Date: Tue, 1 Jul 2025 14:51:11 +0800 Subject: [PATCH 2/4] =?UTF-8?q?refactor(layout):=20=E8=B0=83=E6=95=B4AuthP?= =?UTF-8?q?rovider=E5=92=8CSiteProvider=E7=9A=84=E5=B5=8C=E5=A5=97?= =?UTF-8?q?=E9=A1=BA=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 优化组件结构,将SiteProvider作为AuthProvider的父组件以更好地组织上下文关系 --- src/app/layout.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 30549e4..5aad119 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -41,9 +41,9 @@ export default function RootLayout({ enableSystem disableTransitionOnChange > - - {children} - + + {children} + From 6c81f529535726e2ec9ca251b743aa1368ee3acc Mon Sep 17 00:00:00 2001 From: SongPro Date: Tue, 1 Jul 2025 16:02:33 +0800 Subject: [PATCH 3/4] =?UTF-8?q?feat(=E5=85=AC=E5=91=8A):=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E7=BD=91=E7=AB=99=E5=85=AC=E5=91=8A=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=8F=8A=E5=BC=B9=E7=AA=97=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 从环境变量读取公告内容,提供默认值 - 扩展SiteProvider组件以支持公告功能 - 在首页添加公告弹窗组件,支持本地存储记录用户已读状态 - 弹窗包含关闭功能和完善的样式交互 --- src/app/layout.tsx | 5 +++- src/app/page.tsx | 50 +++++++++++++++++++++++++++++++++ src/components/SiteProvider.tsx | 13 +++++++-- 3 files changed, 64 insertions(+), 4 deletions(-) diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 5aad119..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,7 +44,7 @@ export default function RootLayout({ enableSystem disableTransitionOnChange > - + {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/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} + ); } From cb8c5bfc088fffbd5c441d7583573226a84e0c53 Mon Sep 17 00:00:00 2001 From: SongPro Date: Tue, 1 Jul 2025 17:25:15 +0800 Subject: [PATCH 4/4] =?UTF-8?q?fix(AuthProvider):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E8=AE=A4=E8=AF=81=E9=80=BB=E8=BE=91=E4=B8=AD/api=E8=B7=AF?= =?UTF-8?q?=E5=BE=84=E7=9A=84=E6=A3=80=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除未使用密码时的直接跳转逻辑 --- src/components/AuthProvider.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/components/AuthProvider.tsx b/src/components/AuthProvider.tsx index 381e276..c972a2f 100644 --- a/src/components/AuthProvider.tsx +++ b/src/components/AuthProvider.tsx @@ -18,7 +18,7 @@ export default function AuthProvider({ children }: Props) { const { siteName } = useSite(); const authenticate = useCallback(async () => { // 登录页 - if (pathname.startsWith('/login') || pathname.startsWith('/api/login')) { + if (pathname.startsWith('/login') || pathname.startsWith('/api')) { setIsAuthenticated(true); return; } @@ -30,13 +30,6 @@ export default function AuthProvider({ children }: Props) { ? window.location.pathname + window.location.search : pathname; - // 没有密码直接跳转 - if (!password) { - router.replace(`/login?redirect=${encodeURIComponent(fullPath)}`); - setIsAuthenticated(false); - return; - } - // 尝试认证 try { const res = await fetch('/api/login', {