mirror of
https://github.com/MoonTechLab/LunaTV.git
synced 2026-02-22 10:34:42 +08:00
Merge pull request #22 from OuOumm/main
feat(AuthProvider): 添加认证状态管理和加载界面
This commit is contained in:
@@ -29,6 +29,9 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const siteName = process.env.SITE_NAME || 'MoonTV';
|
||||
const announcement =
|
||||
process.env.ANNOUNCEMENT ||
|
||||
'本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。';
|
||||
|
||||
return (
|
||||
<html lang='zh-CN' suppressHydrationWarning>
|
||||
@@ -41,9 +44,9 @@ export default function RootLayout({
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<AuthProvider>
|
||||
<SiteProvider siteName={siteName}>{children}</SiteProvider>
|
||||
</AuthProvider>
|
||||
<SiteProvider siteName={siteName} announcement={announcement}>
|
||||
<AuthProvider>{children}</AuthProvider>
|
||||
</SiteProvider>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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<DoubanItem[]>([]);
|
||||
const [hotTvShows, setHotTvShows] = useState<DoubanItem[]>([]);
|
||||
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 (
|
||||
<PageLayout>
|
||||
<div className='px-2 sm:px-10 py-4 sm:py-8 overflow-visible'>
|
||||
@@ -233,6 +249,40 @@ function HomeClient() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{announcement && showAnnouncement && (
|
||||
<div
|
||||
className={`fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm dark:bg-black/70 p-4 transition-opacity duration-300 ${
|
||||
showAnnouncement ? '' : 'opacity-0 pointer-events-none'
|
||||
}`}
|
||||
>
|
||||
<div className='w-full max-w-md rounded-xl bg-white p-6 shadow-xl dark:bg-gray-900 transform transition-all duration-300 hover:shadow-2xl'>
|
||||
<div className='flex justify-between items-start mb-4'>
|
||||
<h3 className='text-2xl font-bold tracking-tight text-gray-800 dark:text-white border-b border-green-500 pb-1'>
|
||||
提示
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => handleCloseAnnouncement(announcement)}
|
||||
className='text-gray-400 hover:text-gray-500 dark:text-gray-500 dark:hover:text-white transition-colors'
|
||||
aria-label='关闭'
|
||||
></button>
|
||||
</div>
|
||||
<div className='mb-6'>
|
||||
<div className='relative overflow-hidden rounded-lg mb-4 bg-green-50 dark:bg-green-900/20'>
|
||||
<div className='absolute inset-y-0 left-0 w-1.5 bg-green-500 dark:bg-green-400'></div>
|
||||
<p className='ml-4 text-gray-600 dark:text-gray-300 leading-relaxed'>
|
||||
{announcement}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleCloseAnnouncement(announcement)}
|
||||
className='w-full rounded-lg bg-gradient-to-r from-green-600 to-green-700 px-4 py-3 text-white font-medium shadow-md hover:shadow-lg hover:from-green-700 hover:to-green-800 dark:from-green-600 dark:to-green-700 dark:hover:from-green-700 dark:hover:to-green-800 transition-all duration-300 transform hover:-translate-y-0.5'
|
||||
>
|
||||
我知道了
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<boolean | null>(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 (
|
||||
<div className='relative min-h-screen flex items-center justify-center px-4 overflow-hidden'>
|
||||
<div className='absolute top-4 right-4'>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
<div className='relative z-10 w-full max-w-md rounded-3xl bg-gradient-to-b from-white/90 via-white/70 to-white/40 dark:from-zinc-900/90 dark:via-zinc-900/70 dark:to-zinc-900/40 backdrop-blur-xl shadow-2xl p-10 dark:border dark:border-zinc-800'>
|
||||
<h1 className='text-green-600 tracking-tight text-center text-3xl font-extrabold mb-8 bg-clip-text drop-shadow-sm'>
|
||||
{siteName}
|
||||
</h1>
|
||||
<div className='flex justify-center my-10'>
|
||||
<div className='animate-spin rounded-full h-16 w-16 border-t-4 border-b-4 border-green-500' />
|
||||
</div>
|
||||
<p className='text-gray-700 dark:text-gray-300 font-medium text-lg text-center'>
|
||||
正在验证您的身份,请稍候...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return <>{children}</>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<SiteContext.Provider value={{ siteName }}>{children}</SiteContext.Provider>
|
||||
<SiteContext.Provider value={{ siteName, announcement }}>
|
||||
{children}
|
||||
</SiteContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user