diff --git a/src/app/api/change-password/route.ts b/src/app/api/change-password/route.ts new file mode 100644 index 0000000..66d3174 --- /dev/null +++ b/src/app/api/change-password/route.ts @@ -0,0 +1,72 @@ +/* eslint-disable no-console*/ + +import { NextRequest, NextResponse } from 'next/server'; + +import { getAuthInfoFromCookie } from '@/lib/auth'; +import { getStorage } from '@/lib/db'; +import { IStorage } from '@/lib/types'; + +export const runtime = 'edge'; + +export async function POST(request: NextRequest) { + const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage'; + + // 不支持 localstorage 模式 + if (storageType === 'localstorage') { + return NextResponse.json( + { + error: '不支持本地存储模式修改密码', + }, + { status: 400 } + ); + } + + try { + const body = await request.json(); + const { newPassword } = body; + + // 获取认证信息 + const authInfo = getAuthInfoFromCookie(request); + if (!authInfo || !authInfo.username) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + // 验证新密码 + if (!newPassword || typeof newPassword !== 'string') { + return NextResponse.json({ error: '新密码不得为空' }, { status: 400 }); + } + + const username = authInfo.username; + + // 不允许站长修改密码(站长用户名等于 process.env.USERNAME) + if (username === process.env.USERNAME) { + return NextResponse.json( + { error: '站长不能通过此接口修改密码' }, + { status: 403 } + ); + } + + // 获取存储实例 + const storage: IStorage | null = getStorage(); + if (!storage || typeof storage.changePassword !== 'function') { + return NextResponse.json( + { error: '存储服务不支持修改密码' }, + { status: 500 } + ); + } + + // 修改密码 + await storage.changePassword(username, newPassword); + + return NextResponse.json({ ok: true }); + } catch (error) { + console.error('修改密码失败:', error); + return NextResponse.json( + { + error: '修改密码失败', + details: (error as Error).message, + }, + { status: 500 } + ); + } +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 7a59668..ebe21e4 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -27,6 +27,7 @@ function HomeClient() { const [activeTab, setActiveTab] = useState<'home' | 'favorites'>('home'); const [hotMovies, setHotMovies] = useState([]); const [hotTvShows, setHotTvShows] = useState([]); + const [hotVarietyShows, setHotVarietyShows] = useState([]); const [loading, setLoading] = useState(true); const { announcement } = useSite(); @@ -63,14 +64,15 @@ function HomeClient() { try { setLoading(true); - // 并行获取热门电影和热门剧集 - const [moviesData, tvShowsData] = await Promise.all([ + // 并行获取热门电影、热门剧集和热门综艺 + const [moviesData, tvShowsData, varietyShowsData] = await Promise.all([ getDoubanCategories({ kind: 'movie', category: '热门', type: '全部', }), getDoubanCategories({ kind: 'tv', category: 'tv', type: 'tv' }), + getDoubanCategories({ kind: 'tv', category: 'show', type: 'show' }), ]); if (moviesData.code === 200) { @@ -80,6 +82,10 @@ function HomeClient() { if (tvShowsData.code === 200) { setHotTvShows(tvShowsData.list); } + + if (varietyShowsData.code === 200) { + setHotVarietyShows(varietyShowsData.list); + } } catch (error) { console.error('获取豆瓣数据失败:', error); } finally { @@ -301,6 +307,53 @@ function HomeClient() { ))} + + {/* 热门综艺 */} +
+
+

+ 热门综艺 +

+ + 查看更多 + + +
+ + {loading + ? // 加载状态显示灰色占位数据 + Array.from({ length: 8 }).map((_, index) => ( +
+
+
+
+
+
+ )) + : // 显示真实数据 + hotVarietyShows.map((show, index) => ( +
+ +
+ ))} +
+
)} diff --git a/src/components/LogoutButton.tsx b/src/components/LogoutButton.tsx deleted file mode 100644 index 3ca0b3f..0000000 --- a/src/components/LogoutButton.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/* eslint-disable no-console */ - -'use client'; - -import { LogOut } from 'lucide-react'; -import { useState } from 'react'; - -export const LogoutButton: React.FC = () => { - const [loading, setLoading] = useState(false); - - const handleLogout = async () => { - if (loading) return; - - setLoading(true); - - try { - // 调用注销API来清除cookie - await fetch('/api/logout', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - }); - } catch (error) { - console.error('注销请求失败:', error); - } - - window.location.reload(); - }; - - return ( - - ); -}; diff --git a/src/components/MobileHeader.tsx b/src/components/MobileHeader.tsx index dfbcd3c..4691fea 100644 --- a/src/components/MobileHeader.tsx +++ b/src/components/MobileHeader.tsx @@ -3,10 +3,9 @@ import Link from 'next/link'; import { BackButton } from './BackButton'; -import { LogoutButton } from './LogoutButton'; -import { SettingsButton } from './SettingsButton'; import { useSite } from './SiteProvider'; import { ThemeToggle } from './ThemeToggle'; +import { UserMenu } from './UserMenu'; interface MobileHeaderProps { showBackButton?: boolean; @@ -20,13 +19,12 @@ const MobileHeader = ({ showBackButton = false }: MobileHeaderProps) => { {/* 左侧:返回按钮和设置按钮 */}
{showBackButton && } -
{/* 右侧按钮 */}
- +
diff --git a/src/components/PageLayout.tsx b/src/components/PageLayout.tsx index 03ec8a6..a57bb6a 100644 --- a/src/components/PageLayout.tsx +++ b/src/components/PageLayout.tsx @@ -1,10 +1,9 @@ import { BackButton } from './BackButton'; -import { LogoutButton } from './LogoutButton'; import MobileBottomNav from './MobileBottomNav'; import MobileHeader from './MobileHeader'; -import { SettingsButton } from './SettingsButton'; import Sidebar from './Sidebar'; import { ThemeToggle } from './ThemeToggle'; +import { UserMenu } from './UserMenu'; interface PageLayoutProps { children: React.ReactNode; @@ -35,9 +34,8 @@ const PageLayout = ({ children, activePath = '/' }: PageLayoutProps) => { {/* 桌面端顶部按钮 */}
- - +
{/* 主内容 */} diff --git a/src/components/SettingsButton.tsx b/src/components/UserMenu.tsx similarity index 53% rename from src/components/SettingsButton.tsx rename to src/components/UserMenu.tsx index 02defbb..ec58a72 100644 --- a/src/components/SettingsButton.tsx +++ b/src/components/UserMenu.tsx @@ -1,26 +1,59 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable no-console,@typescript-eslint/no-explicit-any */ 'use client'; -import { Settings, X } from 'lucide-react'; +import { KeyRound, LogOut, Settings, Shield, User, X } from 'lucide-react'; +import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import { createPortal } from 'react-dom'; -export const SettingsButton: React.FC = () => { +import { getAuthInfoFromBrowserCookie } from '@/lib/auth'; + +interface AuthInfo { + username?: string; + role?: 'owner' | 'admin' | 'user'; +} + +export const UserMenu: React.FC = () => { + const router = useRouter(); const [isOpen, setIsOpen] = useState(false); + const [isSettingsOpen, setIsSettingsOpen] = useState(false); + const [isChangePasswordOpen, setIsChangePasswordOpen] = useState(false); + const [authInfo, setAuthInfo] = useState(null); + const [storageType, setStorageType] = useState('localstorage'); + const [mounted, setMounted] = useState(false); + + // 设置相关状态 const [defaultAggregateSearch, setDefaultAggregateSearch] = useState(true); const [doubanProxyUrl, setDoubanProxyUrl] = useState(''); const [imageProxyUrl, setImageProxyUrl] = useState(''); const [enableOptimization, setEnableOptimization] = useState(true); const [enableImageProxy, setEnableImageProxy] = useState(false); const [enableDoubanProxy, setEnableDoubanProxy] = useState(false); - const [mounted, setMounted] = useState(false); + + // 修改密码相关状态 + const [newPassword, setNewPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [passwordLoading, setPasswordLoading] = useState(false); + const [passwordError, setPasswordError] = useState(''); // 确保组件已挂载 useEffect(() => { setMounted(true); }, []); + // 获取认证信息和存储类型 + useEffect(() => { + if (typeof window !== 'undefined') { + const auth = getAuthInfoFromBrowserCookie(); + setAuthInfo(auth); + + const type = + (window as any).RUNTIME_CONFIG?.STORAGE_TYPE || 'localstorage'; + setStorageType(type); + } + }, []); + // 从 localStorage 读取设置 useEffect(() => { if (typeof window !== 'undefined') { @@ -37,7 +70,6 @@ export const SettingsButton: React.FC = () => { if (savedEnableDoubanProxy !== null) { setEnableDoubanProxy(JSON.parse(savedEnableDoubanProxy)); } else if (defaultDoubanProxy) { - // 如果有默认豆瓣代理配置,则默认开启 setEnableDoubanProxy(true); } @@ -54,7 +86,6 @@ export const SettingsButton: React.FC = () => { if (savedEnableImageProxy !== null) { setEnableImageProxy(JSON.parse(savedEnableImageProxy)); } else if (defaultImageProxy) { - // 如果有默认图片代理配置,则默认开启 setEnableImageProxy(true); } @@ -73,7 +104,99 @@ export const SettingsButton: React.FC = () => { } }, []); - // 保存设置到 localStorage + const handleMenuClick = () => { + setIsOpen(!isOpen); + }; + + const handleCloseMenu = () => { + setIsOpen(false); + }; + + const handleLogout = async () => { + try { + await fetch('/api/logout', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }); + } catch (error) { + console.error('注销请求失败:', error); + } + window.location.href = '/'; + }; + + const handleAdminPanel = () => { + router.push('/admin'); + }; + + const handleChangePassword = () => { + setIsOpen(false); + setIsChangePasswordOpen(true); + setNewPassword(''); + setConfirmPassword(''); + setPasswordError(''); + }; + + const handleCloseChangePassword = () => { + setIsChangePasswordOpen(false); + setNewPassword(''); + setConfirmPassword(''); + setPasswordError(''); + }; + + const handleSubmitChangePassword = async () => { + setPasswordError(''); + + // 验证密码 + if (!newPassword) { + setPasswordError('新密码不得为空'); + return; + } + + if (newPassword !== confirmPassword) { + setPasswordError('两次输入的密码不一致'); + return; + } + + setPasswordLoading(true); + + try { + const response = await fetch('/api/change-password', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + newPassword, + }), + }); + + const data = await response.json(); + + if (!response.ok) { + setPasswordError(data.error || '修改密码失败'); + return; + } + + // 修改成功,关闭弹窗并登出 + setIsChangePasswordOpen(false); + await handleLogout(); + } catch (error) { + setPasswordError('网络错误,请稍后重试'); + } finally { + setPasswordLoading(false); + } + }; + + const handleSettings = () => { + setIsOpen(false); + setIsSettingsOpen(true); + }; + + const handleCloseSettings = () => { + setIsSettingsOpen(false); + }; + + // 设置相关的处理函数 const handleAggregateToggle = (value: boolean) => { setDefaultAggregateSearch(value); if (typeof window !== 'undefined') { @@ -116,21 +239,11 @@ export const SettingsButton: React.FC = () => { } }; - const handleSettingsClick = () => { - setIsOpen(!isOpen); - }; - - const handleClosePanel = () => { - setIsOpen(false); - }; - - // 重置所有设置为默认值 const handleResetSettings = () => { const defaultImageProxy = (window as any).RUNTIME_CONFIG?.IMAGE_PROXY || ''; const defaultDoubanProxy = (window as any).RUNTIME_CONFIG?.DOUBAN_PROXY || ''; - // 重置所有状态 setDefaultAggregateSearch(true); setEnableOptimization(true); setDoubanProxyUrl(defaultDoubanProxy); @@ -138,7 +251,6 @@ export const SettingsButton: React.FC = () => { setEnableImageProxy(!!defaultImageProxy); setImageProxyUrl(defaultImageProxy); - // 保存到 localStorage if (typeof window !== 'undefined') { localStorage.setItem('defaultAggregateSearch', JSON.stringify(true)); localStorage.setItem('enableOptimization', JSON.stringify(true)); @@ -155,13 +267,124 @@ export const SettingsButton: React.FC = () => { } }; + // 检查是否显示管理面板按钮 + const showAdminPanel = + authInfo?.role === 'owner' || authInfo?.role === 'admin'; + + // 检查是否显示修改密码按钮 + const showChangePassword = + authInfo?.role !== 'owner' && storageType !== 'localstorage'; + + // 角色中文映射 + const getRoleText = (role?: string) => { + switch (role) { + case 'owner': + return '站长'; + case 'admin': + return '管理员'; + case 'user': + return '用户'; + default: + return ''; + } + }; + + // 菜单面板内容 + const menuPanel = ( + <> + {/* 背景遮罩 - 普通菜单无需模糊 */} +
+ + {/* 菜单面板 */} +
+ {/* 用户信息区域 */} + {authInfo?.username && ( +
+
+
+ + 当前用户 + + {authInfo.role && ( + + {getRoleText(authInfo.role)} + + )} +
+
+ {authInfo.username} +
+
+
+ )} + + {/* 菜单项 */} +
+ {/* 设置按钮 */} + + + {/* 管理面板按钮 */} + {showAdminPanel && ( + + )} + + {/* 修改密码按钮 */} + {showChangePassword && ( + + )} + + {/* 分割线 */} +
+ + {/* 登出按钮 */} + +
+
+ + ); + // 设置面板内容 const settingsPanel = ( <> {/* 背景遮罩 */}
{/* 设置面板 */} @@ -181,7 +404,7 @@ export const SettingsButton: React.FC = () => {
+
+ + {/* 表单 */} +
+ {/* 新密码输入 */} +
+ + setNewPassword(e.target.value)} + disabled={passwordLoading} + /> +
+ + {/* 确认密码输入 */} +
+ + setConfirmPassword(e.target.value)} + disabled={passwordLoading} + /> +
+ + {/* 错误信息 */} + {passwordError && ( +
+ {passwordError} +
+ )} +
+ + {/* 操作按钮 */} +
+ + +
+ + {/* 底部说明 */} +
+

+ 修改密码后需要重新登录 +

+
+ + + ); + return ( <> + {/* 使用 Portal 将菜单面板渲染到 document.body */} + {isOpen && mounted && createPortal(menuPanel, document.body)} + {/* 使用 Portal 将设置面板渲染到 document.body */} - {isOpen && mounted && createPortal(settingsPanel, document.body)} + {isSettingsOpen && mounted && createPortal(settingsPanel, document.body)} + + {/* 使用 Portal 将修改密码面板渲染到 document.body */} + {isChangePasswordOpen && + mounted && + createPortal(changePasswordPanel, document.body)} ); };