/* eslint-disable no-console,@typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ 'use client'; import { Check, ChevronDown, ExternalLink, KeyRound, LogOut, Settings, Shield, User, X, } from 'lucide-react'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import { createPortal } from 'react-dom'; import { getAuthInfoFromBrowserCookie } from '@/lib/auth'; import { CURRENT_VERSION } from '@/lib/version'; import { checkForUpdates, UpdateStatus } from '@/lib/version_check'; import { VersionPanel } from './VersionPanel'; 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 [isVersionPanelOpen, setIsVersionPanelOpen] = useState(false); const [authInfo, setAuthInfo] = useState(null); const [storageType, setStorageType] = useState('localstorage'); const [mounted, setMounted] = useState(false); // Body 滚动锁定 - 使用 overflow 方式避免布局问题 useEffect(() => { if (isSettingsOpen || isChangePasswordOpen) { const body = document.body; const html = document.documentElement; // 保存原始样式 const originalBodyOverflow = body.style.overflow; const originalHtmlOverflow = html.style.overflow; // 只设置 overflow 来阻止滚动 body.style.overflow = 'hidden'; html.style.overflow = 'hidden'; return () => { // 恢复所有原始样式 body.style.overflow = originalBodyOverflow; html.style.overflow = originalHtmlOverflow; }; } }, [isSettingsOpen, isChangePasswordOpen]); // 设置相关状态 const [defaultAggregateSearch, setDefaultAggregateSearch] = useState(true); const [doubanProxyUrl, setDoubanProxyUrl] = useState(''); const [enableOptimization, setEnableOptimization] = useState(true); const [fluidSearch, setFluidSearch] = useState(true); const [liveDirectConnect, setLiveDirectConnect] = useState(false); const [doubanDataSource, setDoubanDataSource] = useState('cmliussss-cdn-tencent'); const [doubanImageProxyType, setDoubanImageProxyType] = useState('cmliussss-cdn-tencent'); const [doubanImageProxyUrl, setDoubanImageProxyUrl] = useState(''); const [isDoubanDropdownOpen, setIsDoubanDropdownOpen] = useState(false); const [isDoubanImageProxyDropdownOpen, setIsDoubanImageProxyDropdownOpen] = useState(false); // 豆瓣数据源选项 const doubanDataSourceOptions = [ { value: 'direct', label: '直连(服务器直接请求豆瓣)' }, { value: 'cors-proxy-zwei', label: 'Cors Proxy By Zwei' }, { value: 'cmliussss-cdn-tencent', label: '豆瓣 CDN By CMLiussss(腾讯云)', }, { value: 'cmliussss-cdn-ali', label: '豆瓣 CDN By CMLiussss(阿里云)' }, { value: 'custom', label: '自定义代理' }, ]; // 豆瓣图片代理选项 const doubanImageProxyTypeOptions = [ { value: 'direct', label: '直连(浏览器直接请求豆瓣)' }, { value: 'server', label: '服务器代理(由服务器代理请求豆瓣)' }, { value: 'img3', label: '豆瓣官方精品 CDN(阿里云)' }, { value: 'cmliussss-cdn-tencent', label: '豆瓣 CDN By CMLiussss(腾讯云)', }, { value: 'cmliussss-cdn-ali', label: '豆瓣 CDN By CMLiussss(阿里云)' }, { value: 'custom', label: '自定义代理' }, ]; // 修改密码相关状态 const [newPassword, setNewPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); const [passwordLoading, setPasswordLoading] = useState(false); const [passwordError, setPasswordError] = useState(''); // 版本检查相关状态 const [updateStatus, setUpdateStatus] = useState(null); const [isChecking, setIsChecking] = useState(true); // 确保组件已挂载 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') { const savedAggregateSearch = localStorage.getItem( 'defaultAggregateSearch' ); if (savedAggregateSearch !== null) { setDefaultAggregateSearch(JSON.parse(savedAggregateSearch)); } const savedDoubanDataSource = localStorage.getItem('doubanDataSource'); const defaultDoubanProxyType = (window as any).RUNTIME_CONFIG?.DOUBAN_PROXY_TYPE || 'cmliussss-cdn-tencent'; if (savedDoubanDataSource !== null) { setDoubanDataSource(savedDoubanDataSource); } else if (defaultDoubanProxyType) { setDoubanDataSource(defaultDoubanProxyType); } const savedDoubanProxyUrl = localStorage.getItem('doubanProxyUrl'); const defaultDoubanProxy = (window as any).RUNTIME_CONFIG?.DOUBAN_PROXY || ''; if (savedDoubanProxyUrl !== null) { setDoubanProxyUrl(savedDoubanProxyUrl); } else if (defaultDoubanProxy) { setDoubanProxyUrl(defaultDoubanProxy); } const savedDoubanImageProxyType = localStorage.getItem( 'doubanImageProxyType' ); const defaultDoubanImageProxyType = (window as any).RUNTIME_CONFIG?.DOUBAN_IMAGE_PROXY_TYPE || 'cmliussss-cdn-tencent'; if (savedDoubanImageProxyType !== null) { setDoubanImageProxyType(savedDoubanImageProxyType); } else if (defaultDoubanImageProxyType) { setDoubanImageProxyType(defaultDoubanImageProxyType); } const savedDoubanImageProxyUrl = localStorage.getItem( 'doubanImageProxyUrl' ); const defaultDoubanImageProxyUrl = (window as any).RUNTIME_CONFIG?.DOUBAN_IMAGE_PROXY || ''; if (savedDoubanImageProxyUrl !== null) { setDoubanImageProxyUrl(savedDoubanImageProxyUrl); } else if (defaultDoubanImageProxyUrl) { setDoubanImageProxyUrl(defaultDoubanImageProxyUrl); } const savedEnableOptimization = localStorage.getItem('enableOptimization'); if (savedEnableOptimization !== null) { setEnableOptimization(JSON.parse(savedEnableOptimization)); } const savedFluidSearch = localStorage.getItem('fluidSearch'); const defaultFluidSearch = (window as any).RUNTIME_CONFIG?.FLUID_SEARCH !== false; if (savedFluidSearch !== null) { setFluidSearch(JSON.parse(savedFluidSearch)); } else if (defaultFluidSearch !== undefined) { setFluidSearch(defaultFluidSearch); } const savedLiveDirectConnect = localStorage.getItem('liveDirectConnect'); if (savedLiveDirectConnect !== null) { setLiveDirectConnect(JSON.parse(savedLiveDirectConnect)); } } }, []); // 版本检查 useEffect(() => { const checkUpdate = async () => { try { const status = await checkForUpdates(); setUpdateStatus(status); } catch (error) { console.warn('版本检查失败:', error); } finally { setIsChecking(false); } }; checkUpdate(); }, []); // 点击外部区域关闭下拉框 useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (isDoubanDropdownOpen) { const target = event.target as Element; if (!target.closest('[data-dropdown="douban-datasource"]')) { setIsDoubanDropdownOpen(false); } } }; if (isDoubanDropdownOpen) { document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); } }, [isDoubanDropdownOpen]); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (isDoubanImageProxyDropdownOpen) { const target = event.target as Element; if (!target.closest('[data-dropdown="douban-image-proxy"]')) { setIsDoubanImageProxyDropdownOpen(false); } } }; if (isDoubanImageProxyDropdownOpen) { document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); } }, [isDoubanImageProxyDropdownOpen]); 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') { localStorage.setItem('defaultAggregateSearch', JSON.stringify(value)); } }; const handleDoubanProxyUrlChange = (value: string) => { setDoubanProxyUrl(value); if (typeof window !== 'undefined') { localStorage.setItem('doubanProxyUrl', value); } }; const handleOptimizationToggle = (value: boolean) => { setEnableOptimization(value); if (typeof window !== 'undefined') { localStorage.setItem('enableOptimization', JSON.stringify(value)); } }; const handleFluidSearchToggle = (value: boolean) => { setFluidSearch(value); if (typeof window !== 'undefined') { localStorage.setItem('fluidSearch', JSON.stringify(value)); } }; const handleLiveDirectConnectToggle = (value: boolean) => { setLiveDirectConnect(value); if (typeof window !== 'undefined') { localStorage.setItem('liveDirectConnect', JSON.stringify(value)); } }; const handleDoubanDataSourceChange = (value: string) => { setDoubanDataSource(value); if (typeof window !== 'undefined') { localStorage.setItem('doubanDataSource', value); } }; const handleDoubanImageProxyTypeChange = (value: string) => { setDoubanImageProxyType(value); if (typeof window !== 'undefined') { localStorage.setItem('doubanImageProxyType', value); } }; const handleDoubanImageProxyUrlChange = (value: string) => { setDoubanImageProxyUrl(value); if (typeof window !== 'undefined') { localStorage.setItem('doubanImageProxyUrl', value); } }; // 获取感谢信息 const getThanksInfo = (dataSource: string) => { switch (dataSource) { case 'cors-proxy-zwei': return { text: 'Thanks to @Zwei', url: 'https://github.com/bestzwei', }; case 'cmliussss-cdn-tencent': case 'cmliussss-cdn-ali': return { text: 'Thanks to @CMLiussss', url: 'https://github.com/cmliu', }; default: return null; } }; const handleResetSettings = () => { const defaultDoubanProxyType = (window as any).RUNTIME_CONFIG?.DOUBAN_PROXY_TYPE || 'cmliussss-cdn-tencent'; const defaultDoubanProxy = (window as any).RUNTIME_CONFIG?.DOUBAN_PROXY || ''; const defaultDoubanImageProxyType = (window as any).RUNTIME_CONFIG?.DOUBAN_IMAGE_PROXY_TYPE || 'cmliussss-cdn-tencent'; const defaultDoubanImageProxyUrl = (window as any).RUNTIME_CONFIG?.DOUBAN_IMAGE_PROXY || ''; const defaultFluidSearch = (window as any).RUNTIME_CONFIG?.FLUID_SEARCH !== false; setDefaultAggregateSearch(true); setEnableOptimization(true); setFluidSearch(defaultFluidSearch); setLiveDirectConnect(false); setDoubanProxyUrl(defaultDoubanProxy); setDoubanDataSource(defaultDoubanProxyType); setDoubanImageProxyType(defaultDoubanImageProxyType); setDoubanImageProxyUrl(defaultDoubanImageProxyUrl); if (typeof window !== 'undefined') { localStorage.setItem('defaultAggregateSearch', JSON.stringify(true)); localStorage.setItem('enableOptimization', JSON.stringify(true)); localStorage.setItem('fluidSearch', JSON.stringify(defaultFluidSearch)); localStorage.setItem('liveDirectConnect', JSON.stringify(false)); localStorage.setItem('doubanProxyUrl', defaultDoubanProxy); localStorage.setItem('doubanDataSource', defaultDoubanProxyType); localStorage.setItem('doubanImageProxyType', defaultDoubanImageProxyType); localStorage.setItem('doubanImageProxyUrl', defaultDoubanImageProxyUrl); } }; // 检查是否显示管理面板按钮 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 = ( <> {/* 背景遮罩 - 普通菜单无需模糊 */}
{/* 菜单面板 */}
{/* 用户信息区域 */}
当前用户 {getRoleText(authInfo?.role || 'user')}
{authInfo?.username || 'default'}
数据存储: {storageType === 'localstorage' ? '本地' : storageType}
{/* 菜单项 */}
{/* 设置按钮 */} {/* 管理面板按钮 */} {showAdminPanel && ( )} {/* 修改密码按钮 */} {showChangePassword && ( )} {/* 分割线 */}
{/* 登出按钮 */} {/* 分割线 */}
{/* 版本信息 */}
); // 设置面板内容 const settingsPanel = ( <> {/* 背景遮罩 */}
{ // 只阻止滚动,允许其他触摸事件 e.preventDefault(); }} onWheel={(e) => { // 阻止滚轮滚动 e.preventDefault(); }} style={{ touchAction: 'none', }} /> {/* 设置面板 */}
{/* 内容容器 - 独立的滚动区域 */}
{/* 标题栏 */}

本地设置

{/* 设置项 */}
{/* 豆瓣数据源选择 */}

豆瓣数据代理

选择获取豆瓣数据的方式

{/* 自定义下拉选择框 */} {/* 下拉箭头 */}
{/* 下拉选项列表 */} {isDoubanDropdownOpen && (
{doubanDataSourceOptions.map((option) => ( ))}
)}
{/* 感谢信息 */} {getThanksInfo(doubanDataSource) && (
)}
{/* 豆瓣代理地址设置 - 仅在选择自定义代理时显示 */} {doubanDataSource === 'custom' && (

豆瓣代理地址

自定义代理服务器地址

handleDoubanProxyUrlChange(e.target.value)} />
)} {/* 分割线 */}
{/* 豆瓣图片代理设置 */}

豆瓣图片代理

选择获取豆瓣图片的方式

{/* 自定义下拉选择框 */} {/* 下拉箭头 */}
{/* 下拉选项列表 */} {isDoubanImageProxyDropdownOpen && (
{doubanImageProxyTypeOptions.map((option) => ( ))}
)}
{/* 感谢信息 */} {getThanksInfo(doubanImageProxyType) && (
)}
{/* 豆瓣图片代理地址设置 - 仅在选择自定义代理时显示 */} {doubanImageProxyType === 'custom' && (

豆瓣图片代理地址

自定义图片代理服务器地址

handleDoubanImageProxyUrlChange(e.target.value) } />
)} {/* 分割线 */}
{/* 默认聚合搜索结果 */}

默认聚合搜索结果

搜索时默认按标题和年份聚合显示结果

{/* 优选和测速 */}

优选和测速

如出现播放器劫持问题可关闭

{/* 流式搜索 */}

流式搜索输出

启用搜索结果实时流式输出,关闭后使用传统一次性搜索

{/* 直播视频浏览器直连 */}

IPTV 视频浏览器直连

开启 IPTV 视频浏览器直连时,需要自备 Allow CORS 插件

{/* 底部说明 */}

这些设置保存在本地浏览器中

); // 修改密码面板内容 const changePasswordPanel = ( <> {/* 背景遮罩 */}
{ // 只阻止滚动,允许其他触摸事件 e.preventDefault(); }} onWheel={(e) => { // 阻止滚轮滚动 e.preventDefault(); }} style={{ touchAction: 'none', }} /> {/* 修改密码面板 */}
{/* 内容容器 - 独立的滚动区域 */}
{ // 阻止事件冒泡到遮罩层,但允许内部滚动 e.stopPropagation(); }} style={{ touchAction: 'auto', // 允许所有触摸操作 }} > {/* 标题栏 */}

修改密码

{/* 表单 */}
{/* 新密码输入 */}
setNewPassword(e.target.value)} disabled={passwordLoading} />
{/* 确认密码输入 */}
setConfirmPassword(e.target.value)} disabled={passwordLoading} />
{/* 错误信息 */} {passwordError && (
{passwordError}
)}
{/* 操作按钮 */}
{/* 底部说明 */}

修改密码后需要重新登录

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