Files
MoonTVPlus/src/components/UserMenu.tsx

2309 lines
96 KiB
TypeScript
Raw Normal View History

2025-08-12 21:50:58 +08:00
/* eslint-disable no-console,@typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
'use client';
import {
2025-12-18 23:31:10 +08:00
Bell,
2025-08-12 21:50:58 +08:00
Check,
ChevronDown,
2026-01-04 16:08:27 +08:00
ChevronUp,
Copy,
2025-12-14 00:22:38 +08:00
Download,
2025-08-12 21:50:58 +08:00
ExternalLink,
2026-01-04 16:29:16 +08:00
Eye,
EyeOff,
Home,
2025-08-12 21:50:58 +08:00
KeyRound,
LogOut,
2026-01-16 16:11:31 +08:00
Mail,
2026-01-04 16:29:16 +08:00
MoveDown,
MoveUp,
Rss,
2025-08-12 21:50:58 +08:00
Settings,
Shield,
Star,
2025-08-12 21:50:58 +08:00
User,
X,
} from 'lucide-react';
import { useRouter } from 'next/navigation';
2025-12-18 23:48:12 +08:00
import { useEffect, useRef, useState } from 'react';
2025-08-12 21:50:58 +08:00
import { createPortal } from 'react-dom';
import { getAuthInfoFromBrowserCookie } from '@/lib/auth';
2025-12-05 20:05:30 +08:00
import { clearAllDanmakuCache } from '@/lib/danmaku/api';
2025-08-15 13:49:08 +08:00
import { CURRENT_VERSION } from '@/lib/version';
2025-12-05 21:12:15 +08:00
import { UpdateStatus } from '@/lib/version_check';
2025-08-12 21:50:58 +08:00
2025-12-05 21:12:15 +08:00
import { useVersionCheck } from './VersionCheckProvider';
2025-08-12 21:50:58 +08:00
import { VersionPanel } from './VersionPanel';
2025-12-14 00:22:38 +08:00
import { OfflineDownloadPanel } from './OfflineDownloadPanel';
2025-12-18 23:31:10 +08:00
import { NotificationPanel } from './NotificationPanel';
import { FavoritesPanel } from './FavoritesPanel';
2025-08-12 21:50:58 +08:00
interface AuthInfo {
username?: string;
role?: 'owner' | 'admin' | 'user';
}
export const UserMenu: React.FC = () => {
const router = useRouter();
2025-12-05 21:12:15 +08:00
const { updateStatus, isChecking } = useVersionCheck();
2025-08-12 21:50:58 +08:00
const [isOpen, setIsOpen] = useState(false);
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const [isChangePasswordOpen, setIsChangePasswordOpen] = useState(false);
const [isSubscribeOpen, setIsSubscribeOpen] = useState(false);
2025-08-12 21:50:58 +08:00
const [isVersionPanelOpen, setIsVersionPanelOpen] = useState(false);
2025-12-14 00:22:38 +08:00
const [isOfflineDownloadPanelOpen, setIsOfflineDownloadPanelOpen] = useState(false);
2025-12-18 23:31:10 +08:00
const [isNotificationPanelOpen, setIsNotificationPanelOpen] = useState(false);
const [isFavoritesPanelOpen, setIsFavoritesPanelOpen] = useState(false);
2026-01-16 16:11:31 +08:00
const [isEmailSettingsOpen, setIsEmailSettingsOpen] = useState(false);
2025-08-12 21:50:58 +08:00
const [authInfo, setAuthInfo] = useState<AuthInfo | null>(null);
const [storageType, setStorageType] = useState<string>('localstorage');
const [mounted, setMounted] = useState(false);
2025-12-18 23:31:10 +08:00
const [unreadCount, setUnreadCount] = useState(0);
2025-08-12 21:50:58 +08:00
// 订阅相关状态
const [subscribeEnabled, setSubscribeEnabled] = useState(false);
const [subscribeUrl, setSubscribeUrl] = useState('');
const [copySuccess, setCopySuccess] = useState(false);
const [adFilterEnabled, setAdFilterEnabled] = useState(true); // 去广告开关,默认开启
2025-08-17 20:34:47 +08:00
// Body 滚动锁定 - 使用 overflow 方式避免布局问题
useEffect(() => {
2026-01-16 16:11:31 +08:00
if (isSettingsOpen || isChangePasswordOpen || isSubscribeOpen || isOfflineDownloadPanelOpen || isEmailSettingsOpen) {
2025-08-17 20:34:47 +08:00
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;
};
}
2026-01-16 16:11:31 +08:00
}, [isSettingsOpen, isChangePasswordOpen, isSubscribeOpen, isOfflineDownloadPanelOpen, isEmailSettingsOpen]);
2025-08-17 20:34:47 +08:00
2025-08-12 21:50:58 +08:00
// 设置相关状态
const [defaultAggregateSearch, setDefaultAggregateSearch] = useState(true);
const [doubanProxyUrl, setDoubanProxyUrl] = useState('');
const [enableOptimization, setEnableOptimization] = useState(true);
2025-08-17 17:32:42 +08:00
const [fluidSearch, setFluidSearch] = useState(true);
2025-08-24 00:26:48 +08:00
const [liveDirectConnect, setLiveDirectConnect] = useState(false);
2025-12-28 00:51:02 +08:00
const [tmdbBackdropDisabled, setTmdbBackdropDisabled] = useState(false);
2026-01-02 01:50:16 +08:00
const [enableTrailers, setEnableTrailers] = useState(false);
2025-08-26 22:53:04 +08:00
const [doubanDataSource, setDoubanDataSource] = useState('cmliussss-cdn-tencent');
const [doubanImageProxyType, setDoubanImageProxyType] = useState('cmliussss-cdn-tencent');
2025-08-12 21:50:58 +08:00
const [doubanImageProxyUrl, setDoubanImageProxyUrl] = useState('');
const [isDoubanDropdownOpen, setIsDoubanDropdownOpen] = useState(false);
const [isDoubanImageProxyDropdownOpen, setIsDoubanImageProxyDropdownOpen] =
useState(false);
2025-12-28 22:11:26 +08:00
const [bufferStrategy, setBufferStrategy] = useState('medium');
const [nextEpisodePreCache, setNextEpisodePreCache] = useState(true);
2026-01-13 14:45:22 +08:00
const [nextEpisodeDanmakuPreload, setNextEpisodeDanmakuPreload] = useState(true);
2026-01-12 23:28:48 +08:00
const [searchTraditionalToSimplified, setSearchTraditionalToSimplified] = useState(false);
2025-08-12 21:50:58 +08:00
2026-01-16 16:11:31 +08:00
// 邮件通知设置
const [userEmail, setUserEmail] = useState('');
const [emailNotifications, setEmailNotifications] = useState(false);
const [emailSettingsLoading, setEmailSettingsLoading] = useState(false);
const [emailSettingsSaving, setEmailSettingsSaving] = useState(false);
2026-01-16 16:11:31 +08:00
2026-01-04 16:08:27 +08:00
// 折叠面板状态
const [isDoubanSectionOpen, setIsDoubanSectionOpen] = useState(true);
const [isUsageSectionOpen, setIsUsageSectionOpen] = useState(false);
const [isBufferSectionOpen, setIsBufferSectionOpen] = useState(false);
const [isDanmakuSectionOpen, setIsDanmakuSectionOpen] = useState(false);
2026-01-04 16:29:16 +08:00
const [isHomepageSectionOpen, setIsHomepageSectionOpen] = useState(false);
// 首页模块配置
interface HomeModule {
id: string;
name: string;
enabled: boolean;
order: number;
}
const defaultHomeModules: HomeModule[] = [
{ id: 'hotMovies', name: '热门电影', enabled: true, order: 0 },
{ id: 'hotDuanju', name: '热播短剧', enabled: true, order: 1 },
{ id: 'bangumiCalendar', name: '新番放送', enabled: true, order: 2 },
{ id: 'hotTvShows', name: '热门剧集', enabled: true, order: 3 },
{ id: 'hotVarietyShows', name: '热门综艺', enabled: true, order: 4 },
{ id: 'upcomingContent', name: '即将上映', enabled: true, order: 5 },
];
const [homeModules, setHomeModules] = useState<HomeModule[]>(defaultHomeModules);
2026-01-04 16:08:27 +08:00
2025-08-12 21:50:58 +08:00
// 豆瓣数据源选项
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: 'server', label: '服务器代理(由服务器代理请求豆瓣)' },
{
value: 'cmliussss-cdn-tencent',
label: '豆瓣 CDN By CMLiussss腾讯云',
},
{ value: 'cmliussss-cdn-ali', label: '豆瓣 CDN By CMLiussss阿里云' },
{ value: 'custom', label: '自定义代理' },
2026-01-09 22:50:15 +08:00
{ value: 'direct', label: '直连(浏览器直接请求豆瓣,可能需要浏览器插件才能正常显示)' },
{ value: 'img3', label: '豆瓣官方精品 CDN阿里云可能需要浏览器插件才能正常显示' },
2025-08-12 21:50:58 +08:00
];
2025-12-28 22:11:26 +08:00
// 缓冲策略选项
const bufferStrategyOptions = [
{ value: 'low', label: '低缓冲(省流量)' },
{ value: 'medium', label: '中缓冲(推荐)' },
{ value: 'high', label: '高缓冲(流畅播放)' },
{ value: 'ultra', label: '超高缓冲(极速体验)' },
];
2025-08-12 21:50:58 +08:00
// 修改密码相关状态
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [passwordLoading, setPasswordLoading] = useState(false);
const [passwordError, setPasswordError] = useState('');
2025-12-05 20:05:30 +08:00
// 清除弹幕缓存相关状态
const [isClearingCache, setIsClearingCache] = useState(false);
const [clearCacheMessage, setClearCacheMessage] = useState<string | null>(null);
2025-08-12 21:50:58 +08:00
// 确保组件已挂载
useEffect(() => {
setMounted(true);
}, []);
2025-12-18 23:31:10 +08:00
// 加载未读通知数量
const loadUnreadCount = async () => {
try {
const response = await fetch('/api/notifications');
if (response.ok) {
const data = await response.json();
2025-12-18 23:48:12 +08:00
const count = data.unreadCount || 0;
setUnreadCount(count);
// 同步到全局,让其他 UserMenu 实例也能获取
if (typeof window !== 'undefined') {
(window as any).__unreadNotificationCount = count;
}
2025-12-18 23:31:10 +08:00
}
} catch (error) {
console.error('加载未读通知数量失败:', error);
}
};
2025-12-18 23:48:12 +08:00
// 首次加载时检查未读通知数量(使用全局标记避免多个实例重复请求)
2025-12-18 23:31:10 +08:00
useEffect(() => {
2025-12-18 23:48:12 +08:00
if (typeof window === 'undefined') return;
// 检查是否已经有其他实例在加载
const globalWindow = window as any;
if (globalWindow.__loadingNotifications) {
// 如果正在加载,等待加载完成后获取结果
const checkInterval = setInterval(() => {
if (!globalWindow.__loadingNotifications && globalWindow.__unreadNotificationCount !== undefined) {
setUnreadCount(globalWindow.__unreadNotificationCount);
clearInterval(checkInterval);
}
}, 100);
return () => clearInterval(checkInterval);
}
// 检查是否已经加载过
if (globalWindow.__unreadNotificationCount !== undefined) {
setUnreadCount(globalWindow.__unreadNotificationCount);
return;
}
// 标记正在加载
globalWindow.__loadingNotifications = true;
loadUnreadCount().finally(() => {
globalWindow.__loadingNotifications = false;
});
2025-12-18 23:31:10 +08:00
}, []);
// 监听通知更新事件
useEffect(() => {
const handleNotificationsUpdated = () => {
2025-12-18 23:48:12 +08:00
// 清除缓存,强制重新加载
if (typeof window !== 'undefined') {
delete (window as any).__unreadNotificationCount;
}
2025-12-18 23:31:10 +08:00
loadUnreadCount();
};
window.addEventListener('notificationsUpdated', handleNotificationsUpdated);
return () => {
window.removeEventListener('notificationsUpdated', handleNotificationsUpdated);
};
}, []);
2025-12-09 21:33:15 +08:00
// 从运行时配置读取订阅是否启用
useEffect(() => {
if (typeof window !== 'undefined') {
2025-12-09 21:33:15 +08:00
const enabled = (window as any).RUNTIME_CONFIG?.ENABLE_TVBOX_SUBSCRIBE || false;
setSubscribeEnabled(enabled);
}
2025-12-09 21:33:15 +08:00
}, []);
// 懒加载订阅 URL - 只在打开订阅面板时请求
const fetchSubscribeUrl = async () => {
try {
const currentOrigin = window.location.origin;
const response = await fetch(`/api/tvbox/config?origin=${encodeURIComponent(currentOrigin)}&adFilter=${adFilterEnabled}`);
if (response.ok) {
const data = await response.json();
setSubscribeUrl(data.url);
}
} catch (error) {
console.error('获取订阅URL失败:', error);
}
};
2025-08-12 21:50:58 +08:00
// 获取认证信息和存储类型
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 =
2025-08-26 22:53:04 +08:00
(window as any).RUNTIME_CONFIG?.DOUBAN_PROXY_TYPE || 'cmliussss-cdn-tencent';
2025-08-12 21:50:58 +08:00
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 =
2025-08-26 22:53:04 +08:00
(window as any).RUNTIME_CONFIG?.DOUBAN_IMAGE_PROXY_TYPE || 'cmliussss-cdn-tencent';
2025-08-12 21:50:58 +08:00
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));
}
2025-08-17 17:32:42 +08:00
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);
}
2025-08-24 00:26:48 +08:00
const savedLiveDirectConnect = localStorage.getItem('liveDirectConnect');
if (savedLiveDirectConnect !== null) {
setLiveDirectConnect(JSON.parse(savedLiveDirectConnect));
}
2025-12-24 21:55:27 +08:00
2025-12-28 00:51:02 +08:00
const savedTmdbBackdropDisabled = localStorage.getItem('tmdb_backdrop_disabled');
if (savedTmdbBackdropDisabled !== null) {
setTmdbBackdropDisabled(savedTmdbBackdropDisabled === 'true');
}
2025-12-28 22:11:26 +08:00
2026-01-02 01:50:16 +08:00
const savedEnableTrailers = localStorage.getItem('enableTrailers');
if (savedEnableTrailers !== null) {
setEnableTrailers(savedEnableTrailers === 'true');
}
2025-12-28 22:11:26 +08:00
const savedBufferStrategy = localStorage.getItem('bufferStrategy');
if (savedBufferStrategy !== null) {
setBufferStrategy(savedBufferStrategy);
}
const savedNextEpisodePreCache = localStorage.getItem('nextEpisodePreCache');
if (savedNextEpisodePreCache !== null) {
setNextEpisodePreCache(savedNextEpisodePreCache === 'true');
}
2026-01-04 16:29:16 +08:00
2026-01-13 14:45:22 +08:00
const savedNextEpisodeDanmakuPreload = localStorage.getItem('nextEpisodeDanmakuPreload');
if (savedNextEpisodeDanmakuPreload !== null) {
setNextEpisodeDanmakuPreload(savedNextEpisodeDanmakuPreload === 'true');
}
2026-01-04 16:29:16 +08:00
// 加载首页模块配置
const savedHomeModules = localStorage.getItem('homeModules');
if (savedHomeModules !== null) {
try {
setHomeModules(JSON.parse(savedHomeModules));
} catch (error) {
console.error('解析首页模块配置失败:', error);
}
}
2026-01-12 23:28:48 +08:00
// 加载搜索繁体转简体设置
const savedSearchTraditionalToSimplified = localStorage.getItem('searchTraditionalToSimplified');
if (savedSearchTraditionalToSimplified !== null) {
setSearchTraditionalToSimplified(savedSearchTraditionalToSimplified === 'true');
}
2025-08-12 21:50:58 +08:00
}
}, []);
2026-01-16 16:11:31 +08:00
// 加载邮件通知设置
const loadEmailSettings = async () => {
setEmailSettingsLoading(true);
2026-01-16 16:11:31 +08:00
try {
const response = await fetch('/api/user/email-settings');
if (response.ok) {
const data = await response.json();
setUserEmail(data.email || '');
setEmailNotifications(data.emailNotifications || false);
}
} catch (error) {
console.error('加载邮件设置失败:', error);
} finally {
setEmailSettingsLoading(false);
2026-01-16 16:11:31 +08:00
}
};
// 保存邮件通知设置
const handleSaveEmailSettings = async () => {
setEmailSettingsSaving(true);
2026-01-16 16:11:31 +08:00
try {
const response = await fetch('/api/user/email-settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: userEmail,
emailNotifications,
}),
});
const messageEl = document.getElementById('email-settings-message');
if (response.ok) {
if (messageEl) {
messageEl.textContent = '保存成功!';
messageEl.className = 'text-xs text-center text-green-600 dark:text-green-400';
messageEl.classList.remove('hidden');
setTimeout(() => {
messageEl.classList.add('hidden');
}, 3000);
}
} else {
const data = await response.json();
if (messageEl) {
messageEl.textContent = data.error || '保存失败';
messageEl.className = 'text-xs text-center text-red-600 dark:text-red-400';
messageEl.classList.remove('hidden');
}
}
} catch (error) {
console.error('保存邮件设置失败:', error);
const messageEl = document.getElementById('email-settings-message');
if (messageEl) {
messageEl.textContent = '保存失败,请重试';
messageEl.className = 'text-xs text-center text-red-600 dark:text-red-400';
messageEl.classList.remove('hidden');
}
} finally {
setEmailSettingsSaving(false);
2026-01-16 16:11:31 +08:00
}
};
2025-08-12 21:50:58 +08:00
// 点击外部区域关闭下拉框
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('');
};
2025-12-09 21:33:15 +08:00
const handleSubscribe = async () => {
setIsOpen(false);
setIsSubscribeOpen(true);
setCopySuccess(false);
2025-12-09 21:33:15 +08:00
// 懒加载:打开面板时才请求订阅URL
await fetchSubscribeUrl();
};
const handleCloseSubscribe = () => {
setIsSubscribeOpen(false);
setCopySuccess(false);
};
2025-12-09 21:33:15 +08:00
const handleAdFilterToggle = async (checked: boolean) => {
setAdFilterEnabled(checked);
// 当去广告开关改变时,重新获取订阅URL
await fetchSubscribeUrl();
};
const handleCopySubscribeUrl = async () => {
try {
await navigator.clipboard.writeText(subscribeUrl);
setCopySuccess(true);
setTimeout(() => {
setCopySuccess(false);
}, 2000);
} catch (error) {
console.error('复制失败:', error);
}
};
2025-08-12 21:50:58 +08:00
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));
}
};
2025-08-17 17:32:42 +08:00
const handleFluidSearchToggle = (value: boolean) => {
setFluidSearch(value);
if (typeof window !== 'undefined') {
localStorage.setItem('fluidSearch', JSON.stringify(value));
}
};
2025-08-24 00:26:48 +08:00
const handleLiveDirectConnectToggle = (value: boolean) => {
setLiveDirectConnect(value);
if (typeof window !== 'undefined') {
localStorage.setItem('liveDirectConnect', JSON.stringify(value));
}
};
2025-12-28 00:51:02 +08:00
const handleTmdbBackdropDisabledToggle = (value: boolean) => {
setTmdbBackdropDisabled(value);
if (typeof window !== 'undefined') {
localStorage.setItem('tmdb_backdrop_disabled', String(value));
}
};
2026-01-02 01:50:16 +08:00
const handleEnableTrailersToggle = (value: boolean) => {
setEnableTrailers(value);
if (typeof window !== 'undefined') {
localStorage.setItem('enableTrailers', String(value));
}
};
2025-08-12 21:50:58 +08:00
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);
}
};
2025-12-28 22:11:26 +08:00
const handleBufferStrategyChange = (value: string) => {
setBufferStrategy(value);
if (typeof window !== 'undefined') {
localStorage.setItem('bufferStrategy', value);
}
};
2026-01-15 12:43:40 +08:00
// 将滑块值转换为策略值
const getBufferStrategyFromSlider = (sliderValue: number): string => {
const strategies = ['low', 'medium', 'high', 'ultra'];
return strategies[sliderValue] || 'medium';
};
// 将策略值转换为滑块值
const getSliderValueFromStrategy = (strategy: string): number => {
const strategies = ['low', 'medium', 'high', 'ultra'];
const index = strategies.indexOf(strategy);
return index >= 0 ? index : 1; // 默认返回 1 (medium)
};
2025-12-28 22:11:26 +08:00
const handleNextEpisodePreCacheToggle = (value: boolean) => {
setNextEpisodePreCache(value);
if (typeof window !== 'undefined') {
localStorage.setItem('nextEpisodePreCache', String(value));
}
};
2026-01-13 14:45:22 +08:00
const handleNextEpisodeDanmakuPreloadToggle = (value: boolean) => {
setNextEpisodeDanmakuPreload(value);
if (typeof window !== 'undefined') {
localStorage.setItem('nextEpisodeDanmakuPreload', String(value));
}
};
2026-01-12 23:28:48 +08:00
const handleSearchTraditionalToSimplifiedToggle = (value: boolean) => {
setSearchTraditionalToSimplified(value);
if (typeof window !== 'undefined') {
localStorage.setItem('searchTraditionalToSimplified', String(value));
}
};
2026-01-04 16:29:16 +08:00
// 首页模块配置处理函数
const handleHomeModuleToggle = (id: string, enabled: boolean) => {
const updatedModules = homeModules.map(module =>
module.id === id ? { ...module, enabled } : module
);
setHomeModules(updatedModules);
if (typeof window !== 'undefined') {
localStorage.setItem('homeModules', JSON.stringify(updatedModules));
// 触发自定义事件通知首页刷新
window.dispatchEvent(new CustomEvent('homeModulesUpdated'));
}
};
const handleHomeModuleMoveUp = (index: number) => {
if (index === 0) return;
const updatedModules = [...homeModules];
const temp = updatedModules[index];
updatedModules[index] = updatedModules[index - 1];
updatedModules[index - 1] = temp;
// 更新order
updatedModules.forEach((module, idx) => {
module.order = idx;
});
setHomeModules(updatedModules);
if (typeof window !== 'undefined') {
localStorage.setItem('homeModules', JSON.stringify(updatedModules));
window.dispatchEvent(new CustomEvent('homeModulesUpdated'));
}
};
const handleHomeModuleMoveDown = (index: number) => {
if (index === homeModules.length - 1) return;
const updatedModules = [...homeModules];
const temp = updatedModules[index];
updatedModules[index] = updatedModules[index + 1];
updatedModules[index + 1] = temp;
// 更新order
updatedModules.forEach((module, idx) => {
module.order = idx;
});
setHomeModules(updatedModules);
if (typeof window !== 'undefined') {
localStorage.setItem('homeModules', JSON.stringify(updatedModules));
window.dispatchEvent(new CustomEvent('homeModulesUpdated'));
}
};
2025-08-12 21:50:58 +08:00
// 获取感谢信息
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 =
2025-08-26 22:53:04 +08:00
(window as any).RUNTIME_CONFIG?.DOUBAN_PROXY_TYPE || 'cmliussss-cdn-tencent';
2025-08-12 21:50:58 +08:00
const defaultDoubanProxy =
(window as any).RUNTIME_CONFIG?.DOUBAN_PROXY || '';
const defaultDoubanImageProxyType =
2025-08-26 22:53:04 +08:00
(window as any).RUNTIME_CONFIG?.DOUBAN_IMAGE_PROXY_TYPE || 'cmliussss-cdn-tencent';
2025-08-12 21:50:58 +08:00
const defaultDoubanImageProxyUrl =
(window as any).RUNTIME_CONFIG?.DOUBAN_IMAGE_PROXY || '';
2025-08-17 17:32:42 +08:00
const defaultFluidSearch =
(window as any).RUNTIME_CONFIG?.FLUID_SEARCH !== false;
2025-08-12 21:50:58 +08:00
setDefaultAggregateSearch(true);
setEnableOptimization(true);
2025-08-17 17:32:42 +08:00
setFluidSearch(defaultFluidSearch);
2025-08-24 00:26:48 +08:00
setLiveDirectConnect(false);
2026-01-02 01:50:16 +08:00
setTmdbBackdropDisabled(false);
setEnableTrailers(false);
2025-08-12 21:50:58 +08:00
setDoubanProxyUrl(defaultDoubanProxy);
setDoubanDataSource(defaultDoubanProxyType);
setDoubanImageProxyType(defaultDoubanImageProxyType);
setDoubanImageProxyUrl(defaultDoubanImageProxyUrl);
2025-12-28 22:11:26 +08:00
setBufferStrategy('medium');
setNextEpisodePreCache(true);
2026-01-13 14:45:22 +08:00
setNextEpisodeDanmakuPreload(true);
2026-01-04 16:29:16 +08:00
setHomeModules(defaultHomeModules);
2026-01-12 23:28:48 +08:00
setSearchTraditionalToSimplified(false);
2025-08-12 21:50:58 +08:00
if (typeof window !== 'undefined') {
localStorage.setItem('defaultAggregateSearch', JSON.stringify(true));
localStorage.setItem('enableOptimization', JSON.stringify(true));
2025-08-17 17:32:42 +08:00
localStorage.setItem('fluidSearch', JSON.stringify(defaultFluidSearch));
2025-08-24 00:26:48 +08:00
localStorage.setItem('liveDirectConnect', JSON.stringify(false));
2026-01-02 01:50:16 +08:00
localStorage.setItem('tmdb_backdrop_disabled', 'false');
localStorage.setItem('enableTrailers', 'false');
2025-08-12 21:50:58 +08:00
localStorage.setItem('doubanProxyUrl', defaultDoubanProxy);
localStorage.setItem('doubanDataSource', defaultDoubanProxyType);
localStorage.setItem('doubanImageProxyType', defaultDoubanImageProxyType);
localStorage.setItem('doubanImageProxyUrl', defaultDoubanImageProxyUrl);
2025-12-28 22:11:26 +08:00
localStorage.setItem('bufferStrategy', 'medium');
localStorage.setItem('nextEpisodePreCache', 'true');
2026-01-13 14:45:22 +08:00
localStorage.setItem('nextEpisodeDanmakuPreload', 'true');
2026-01-04 16:29:16 +08:00
localStorage.setItem('homeModules', JSON.stringify(defaultHomeModules));
2026-01-12 23:28:48 +08:00
localStorage.setItem('searchTraditionalToSimplified', 'false');
2026-01-04 16:29:16 +08:00
window.dispatchEvent(new CustomEvent('homeModulesUpdated'));
2025-08-12 21:50:58 +08:00
}
};
2025-12-05 20:05:30 +08:00
// 清除弹幕缓存
const handleClearDanmakuCache = async () => {
setIsClearingCache(true);
setClearCacheMessage(null);
try {
await clearAllDanmakuCache();
setClearCacheMessage('弹幕缓存已清除成功!');
console.log('弹幕缓存已清除');
// 3秒后自动清除提示
setTimeout(() => {
setClearCacheMessage(null);
}, 3000);
} catch (error) {
console.error('清除弹幕缓存失败:', error);
setClearCacheMessage('清除失败,请重试');
// 3秒后自动清除提示
setTimeout(() => {
setClearCacheMessage(null);
}, 3000);
} finally {
setIsClearingCache(false);
}
};
2025-08-12 21:50:58 +08:00
// 检查是否显示管理面板按钮
const showAdminPanel =
authInfo?.role === 'owner' || authInfo?.role === 'admin';
2025-12-14 00:22:38 +08:00
// 检查是否显示离线下载按钮
const showOfflineDownload =
(authInfo?.role === 'owner' || authInfo?.role === 'admin') &&
typeof window !== 'undefined' &&
2025-12-17 21:24:44 +08:00
(window as any).RUNTIME_CONFIG?.ENABLE_OFFLINE_DOWNLOAD === true;
2025-12-14 00:22:38 +08:00
2025-08-12 21:50:58 +08:00
// 检查是否显示修改密码按钮
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 = (
<>
{/* 背景遮罩 - 普通菜单无需模糊 */}
<div
className='fixed inset-0 bg-transparent z-[1000]'
onClick={handleCloseMenu}
/>
{/* 菜单面板 */}
<div className='fixed top-14 right-4 w-56 bg-white dark:bg-gray-900 rounded-lg shadow-xl z-[1001] border border-gray-200/50 dark:border-gray-700/50 overflow-hidden select-none'>
{/* 用户信息区域 */}
<div className='px-3 py-2.5 border-b border-gray-200 dark:border-gray-700 bg-gradient-to-r from-gray-50 to-gray-100/50 dark:from-gray-800 dark:to-gray-800/50'>
<div className='space-y-1'>
<div className='flex items-center justify-between'>
2026-01-16 16:11:31 +08:00
<div className='flex items-center gap-1.5'>
<span className='text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider'>
</span>
{/* 邮件设置图标按钮 */}
<button
onClick={(e) => {
e.stopPropagation();
setIsOpen(false);
setIsEmailSettingsOpen(true);
// 懒加载:打开面板时才请求邮件设置
loadEmailSettings();
2026-01-16 16:11:31 +08:00
}}
className='p-0.5 rounded hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors'
title='邮件通知设置'
>
<Mail className='w-3 h-3 text-gray-500 dark:text-gray-400' />
</button>
</div>
2025-08-12 21:50:58 +08:00
<span
2025-08-13 22:07:28 +08:00
className={`inline-flex items-center px-1.5 py-0.5 rounded-full text-xs font-medium ${(authInfo?.role || 'user') === 'owner'
2025-08-15 13:49:08 +08:00
? 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300'
: (authInfo?.role || 'user') === 'admin'
? 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300'
: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300'
2025-08-13 22:07:28 +08:00
}`}
2025-08-12 21:50:58 +08:00
>
{getRoleText(authInfo?.role || 'user')}
</span>
</div>
<div className='flex items-center justify-between'>
<div className='font-semibold text-gray-900 dark:text-gray-100 text-sm truncate'>
{authInfo?.username || 'default'}
</div>
<div className='text-[10px] text-gray-400 dark:text-gray-500'>
{storageType === 'localstorage' ? '本地' : storageType}
</div>
</div>
</div>
</div>
{/* 菜单项 */}
<div className='py-1'>
2025-12-18 23:31:10 +08:00
{/* 通知按钮 */}
<button
onClick={() => {
setIsOpen(false);
setIsNotificationPanelOpen(true);
}}
className='w-full px-3 py-2 text-left flex items-center gap-2.5 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors text-sm relative'
>
<Bell className='w-4 h-4 text-gray-500 dark:text-gray-400' />
<span className='font-medium'></span>
{unreadCount > 0 && (
<span className='ml-auto px-2 py-0.5 text-xs font-medium bg-red-500 text-white rounded-full'>
{unreadCount > 99 ? '99+' : unreadCount}
</span>
)}
</button>
{/* 我的收藏按钮 */}
<button
onClick={() => {
setIsOpen(false);
setIsFavoritesPanelOpen(true);
}}
className='w-full px-3 py-2 text-left flex items-center gap-2.5 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors text-sm relative'
>
<Star className='w-4 h-4 text-gray-500 dark:text-gray-400' />
<span className='font-medium'></span>
</button>
2025-08-12 21:50:58 +08:00
{/* 设置按钮 */}
<button
onClick={handleSettings}
className='w-full px-3 py-2 text-left flex items-center gap-2.5 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors text-sm'
>
<Settings className='w-4 h-4 text-gray-500 dark:text-gray-400' />
<span className='font-medium'></span>
</button>
{/* 管理面板按钮 */}
{showAdminPanel && (
<button
onClick={handleAdminPanel}
className='w-full px-3 py-2 text-left flex items-center gap-2.5 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors text-sm'
>
<Shield className='w-4 h-4 text-gray-500 dark:text-gray-400' />
<span className='font-medium'></span>
</button>
)}
2025-12-14 00:22:38 +08:00
{/* 离线下载按钮 */}
{showOfflineDownload && (
<button
onClick={() => {
setIsOfflineDownloadPanelOpen(true);
setIsOpen(false);
}}
className='w-full px-3 py-2 text-left flex items-center gap-2.5 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors text-sm'
>
<Download className='w-4 h-4 text-gray-500 dark:text-gray-400' />
<span className='font-medium'>线</span>
</button>
)}
2025-08-12 21:50:58 +08:00
{/* 修改密码按钮 */}
{showChangePassword && (
<button
onClick={handleChangePassword}
className='w-full px-3 py-2 text-left flex items-center gap-2.5 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors text-sm'
>
<KeyRound className='w-4 h-4 text-gray-500 dark:text-gray-400' />
<span className='font-medium'></span>
</button>
)}
{/* 订阅按钮 */}
{subscribeEnabled && (
<button
onClick={handleSubscribe}
className='w-full px-3 py-2 text-left flex items-center gap-2.5 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors text-sm'
>
<Rss className='w-4 h-4 text-gray-500 dark:text-gray-400' />
<span className='font-medium'></span>
</button>
)}
2025-08-12 21:50:58 +08:00
{/* 分割线 */}
<div className='my-1 border-t border-gray-200 dark:border-gray-700'></div>
{/* 登出按钮 */}
<button
onClick={handleLogout}
className='w-full px-3 py-2 text-left flex items-center gap-2.5 text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors text-sm'
>
<LogOut className='w-4 h-4' />
<span className='font-medium'></span>
</button>
{/* 分割线 */}
<div className='my-1 border-t border-gray-200 dark:border-gray-700'></div>
{/* 版本信息 */}
<button
onClick={() => {
setIsVersionPanelOpen(true);
handleCloseMenu();
}}
className='w-full px-3 py-2 text-center flex items-center justify-center text-gray-500 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors text-xs'
>
<div className='flex items-center gap-1'>
<span className='font-mono'>v{CURRENT_VERSION}</span>
{!isChecking &&
updateStatus &&
updateStatus !== UpdateStatus.FETCH_FAILED && (
<div
2025-08-13 22:07:28 +08:00
className={`w-2 h-2 rounded-full -translate-y-2 ${updateStatus === UpdateStatus.HAS_UPDATE
2025-08-15 13:49:08 +08:00
? 'bg-yellow-500'
: updateStatus === UpdateStatus.NO_UPDATE
? 'bg-green-400'
: ''
2025-08-13 22:07:28 +08:00
}`}
2025-08-12 21:50:58 +08:00
></div>
)}
</div>
</button>
</div>
</div>
</>
);
// 设置面板内容
const settingsPanel = (
<>
{/* 背景遮罩 */}
<div
className='fixed inset-0 bg-black/50 backdrop-blur-sm z-[1000]'
onClick={handleCloseSettings}
2025-08-17 20:34:47 +08:00
onTouchMove={(e) => {
// 只阻止滚动,允许其他触摸事件
e.preventDefault();
}}
onWheel={(e) => {
// 阻止滚轮滚动
e.preventDefault();
}}
style={{
touchAction: 'none',
}}
2025-08-12 21:50:58 +08:00
/>
{/* 设置面板 */}
2025-08-17 20:34:47 +08:00
<div
className='fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-xl max-h-[90vh] bg-white dark:bg-gray-900 rounded-xl shadow-xl z-[1001] flex flex-col'
>
{/* 内容容器 - 独立的滚动区域 */}
<div
2026-01-04 16:08:27 +08:00
className='flex-1 px-4 py-6 md:p-6 overflow-y-auto'
2025-08-17 20:34:47 +08:00
data-panel-content
style={{
touchAction: 'pan-y', // 只允许垂直滚动
overscrollBehavior: 'contain', // 防止滚动冒泡
}}
>
{/* 标题栏 */}
<div className='flex items-center justify-between mb-6'>
<div className='flex items-center gap-3'>
<h3 className='text-xl font-bold text-gray-800 dark:text-gray-200'>
</h3>
<button
onClick={handleResetSettings}
className='px-2 py-1 text-xs text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 border border-red-200 hover:border-red-300 dark:border-red-800 dark:hover:border-red-700 hover:bg-red-50 dark:hover:bg-red-900/20 rounded transition-colors'
title='重置为默认设置'
>
</button>
</div>
2025-08-12 21:50:58 +08:00
<button
2025-08-17 20:34:47 +08:00
onClick={handleCloseSettings}
className='w-8 h-8 p-1 rounded-full flex items-center justify-center text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors'
aria-label='Close'
2025-08-12 21:50:58 +08:00
>
2025-08-17 20:34:47 +08:00
<X className='w-full h-full' />
2025-08-12 21:50:58 +08:00
</button>
</div>
2025-08-17 20:34:47 +08:00
{/* 设置项 */}
2026-01-04 16:08:27 +08:00
<div className='space-y-3 md:space-y-4'>
{/* 豆瓣设置 */}
<div className='border border-gray-200 dark:border-gray-700 rounded-lg overflow-visible'>
<button
onClick={() => setIsDoubanSectionOpen(!isDoubanSectionOpen)}
className='w-full px-3 py-2.5 md:px-4 md:py-3 bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-750 transition-colors flex items-center justify-between'
>
<h3 className='text-base font-semibold text-gray-800 dark:text-gray-200'>
</h3>
{isDoubanSectionOpen ? (
<ChevronUp className='w-5 h-5 text-gray-600 dark:text-gray-400' />
) : (
<ChevronDown className='w-5 h-5 text-gray-600 dark:text-gray-400' />
)}
</button>
{isDoubanSectionOpen && (
<div className='p-3 md:p-4 space-y-4 md:space-y-6'>
{/* 豆瓣数据源选择 */}
<div className='space-y-3'>
<div>
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300'>
</h4>
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
</p>
</div>
<div className='relative' data-dropdown='douban-datasource'>
{/* 自定义下拉选择框 */}
2025-08-17 20:34:47 +08:00
<button
type='button'
2026-01-04 16:08:27 +08:00
onClick={() => setIsDoubanDropdownOpen(!isDoubanDropdownOpen)}
className='w-full px-3 py-2.5 pr-10 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-all duration-200 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-sm hover:border-gray-400 dark:hover:border-gray-500 text-left'
2025-08-17 20:34:47 +08:00
>
2026-01-04 16:08:27 +08:00
{
doubanDataSourceOptions.find(
(option) => option.value === doubanDataSource
)?.label
}
2025-08-17 20:34:47 +08:00
</button>
2025-08-12 21:50:58 +08:00
2026-01-04 16:08:27 +08:00
{/* 下拉箭头 */}
<div className='absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none'>
<ChevronDown
className={`w-4 h-4 text-gray-400 dark:text-gray-500 transition-transform duration-200 ${isDoubanDropdownOpen ? 'rotate-180' : ''
}`}
/>
</div>
{/* 下拉选项列表 */}
{isDoubanDropdownOpen && (
<div className='absolute z-50 w-full mt-1 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg shadow-lg max-h-60 overflow-auto'>
{doubanDataSourceOptions.map((option) => (
<button
key={option.value}
type='button'
onClick={() => {
handleDoubanDataSourceChange(option.value);
setIsDoubanDropdownOpen(false);
}}
className={`w-full px-3 py-2.5 text-left text-sm transition-colors duration-150 flex items-center justify-between hover:bg-gray-100 dark:hover:bg-gray-700 ${doubanDataSource === option.value
? 'bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400'
: 'text-gray-900 dark:text-gray-100'
}`}
>
<span className='truncate'>{option.label}</span>
{doubanDataSource === option.value && (
<Check className='w-4 h-4 text-green-600 dark:text-green-400 flex-shrink-0 ml-2' />
)}
</button>
))}
</div>
)}
</div>
2025-08-12 21:50:58 +08:00
2026-01-04 16:08:27 +08:00
{/* 感谢信息 */}
{getThanksInfo(doubanDataSource) && (
<div className='mt-3'>
<button
type='button'
onClick={() =>
window.open(getThanksInfo(doubanDataSource)!.url, '_blank')
}
className='flex items-center justify-center gap-1.5 w-full px-3 text-xs text-gray-500 dark:text-gray-400 cursor-pointer'
>
<span className='font-medium'>
{getThanksInfo(doubanDataSource)!.text}
</span>
<ExternalLink className='w-3.5 opacity-70' />
</button>
</div>
)}
</div>
2025-08-12 21:50:58 +08:00
2026-01-04 16:08:27 +08:00
{/* 豆瓣代理地址设置 - 仅在选择自定义代理时显示 */}
{doubanDataSource === 'custom' && (
<div className='space-y-3'>
<div>
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300'>
</h4>
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
</p>
</div>
<input
type='text'
className='w-full px-3 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-all duration-200 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 shadow-sm hover:border-gray-400 dark:hover:border-gray-500'
placeholder='例如: https://proxy.example.com/fetch?url='
value={doubanProxyUrl}
onChange={(e) => handleDoubanProxyUrlChange(e.target.value)}
/>
</div>
)}
2025-08-12 21:50:58 +08:00
2026-01-04 16:08:27 +08:00
{/* 分割线 */}
<div className='border-t border-gray-200 dark:border-gray-700'></div>
{/* 豆瓣图片代理设置 */}
<div className='space-y-3'>
<div>
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300'>
</h4>
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
</p>
</div>
<div className='relative' data-dropdown='douban-image-proxy'>
{/* 自定义下拉选择框 */}
2025-08-17 20:34:47 +08:00
<button
type='button'
2026-01-04 16:08:27 +08:00
onClick={() =>
setIsDoubanImageProxyDropdownOpen(
!isDoubanImageProxyDropdownOpen
)
}
className='w-full px-3 py-2.5 pr-10 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-all duration-200 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-sm hover:border-gray-400 dark:hover:border-gray-500 text-left'
2025-08-17 20:34:47 +08:00
>
2026-01-04 16:08:27 +08:00
{
doubanImageProxyTypeOptions.find(
(option) => option.value === doubanImageProxyType
)?.label
}
2025-08-17 20:34:47 +08:00
</button>
2026-01-04 16:08:27 +08:00
{/* 下拉箭头 */}
<div className='absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none'>
<ChevronDown
className={`w-4 h-4 text-gray-400 dark:text-gray-500 transition-transform duration-200 ${isDoubanDropdownOpen ? 'rotate-180' : ''
}`}
/>
</div>
{/* 下拉选项列表 */}
{isDoubanImageProxyDropdownOpen && (
<div className='absolute z-50 w-full mt-1 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg shadow-lg max-h-60 overflow-auto'>
{doubanImageProxyTypeOptions.map((option) => (
<button
key={option.value}
type='button'
onClick={() => {
handleDoubanImageProxyTypeChange(option.value);
setIsDoubanImageProxyDropdownOpen(false);
}}
className={`w-full px-3 py-2.5 text-left text-sm transition-colors duration-150 flex items-center justify-between hover:bg-gray-100 dark:hover:bg-gray-700 ${doubanImageProxyType === option.value
? 'bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400'
: 'text-gray-900 dark:text-gray-100'
}`}
>
<span className='truncate'>{option.label}</span>
{doubanImageProxyType === option.value && (
<Check className='w-4 h-4 text-green-600 dark:text-green-400 flex-shrink-0 ml-2' />
)}
</button>
))}
</div>
)}
</div>
{/* 感谢信息 */}
{getThanksInfo(doubanImageProxyType) && (
<div className='mt-3'>
<button
type='button'
onClick={() =>
window.open(
getThanksInfo(doubanImageProxyType)!.url,
'_blank'
)
}
className='flex items-center justify-center gap-1.5 w-full px-3 text-xs text-gray-500 dark:text-gray-400 cursor-pointer'
>
<span className='font-medium'>
{getThanksInfo(doubanImageProxyType)!.text}
</span>
<ExternalLink className='w-3.5 opacity-70' />
</button>
</div>
)}
2025-08-17 20:34:47 +08:00
</div>
2025-08-12 21:50:58 +08:00
2026-01-04 16:08:27 +08:00
{/* 豆瓣图片代理地址设置 - 仅在选择自定义代理时显示 */}
{doubanImageProxyType === 'custom' && (
<div className='space-y-3'>
<div>
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300'>
</h4>
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
</p>
</div>
<input
type='text'
className='w-full px-3 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-all duration-200 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 shadow-sm hover:border-gray-400 dark:hover:border-gray-500'
placeholder='例如: https://proxy.example.com/fetch?url='
value={doubanImageProxyUrl}
onChange={(e) =>
handleDoubanImageProxyUrlChange(e.target.value)
}
/>
</div>
)}
2025-08-12 21:50:58 +08:00
</div>
)}
</div>
2026-01-04 16:08:27 +08:00
<div className='border border-gray-200 dark:border-gray-700 rounded-lg overflow-visible'>
<button
onClick={() => setIsUsageSectionOpen(!isUsageSectionOpen)}
className='w-full px-3 py-2.5 md:px-4 md:py-3 bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-750 transition-colors flex items-center justify-between'
>
<h3 className='text-base font-semibold text-gray-800 dark:text-gray-200'>
2026-01-07 00:29:49 +08:00
2026-01-04 16:08:27 +08:00
</h3>
{isUsageSectionOpen ? (
<ChevronUp className='w-5 h-5 text-gray-600 dark:text-gray-400' />
) : (
<ChevronDown className='w-5 h-5 text-gray-600 dark:text-gray-400' />
)}
</button>
{isUsageSectionOpen && (
<div className='p-3 md:p-4 space-y-4 md:space-y-6'>
{/* 默认聚合搜索结果 */}
<div className='flex items-center justify-between'>
<div>
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300'>
</h4>
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
</p>
</div>
<label className='flex items-center cursor-pointer'>
<div className='relative'>
<input
type='checkbox'
className='sr-only peer'
checked={defaultAggregateSearch}
onChange={(e) => handleAggregateToggle(e.target.checked)}
/>
<div className='w-11 h-6 bg-gray-300 rounded-full peer-checked:bg-green-500 transition-colors dark:bg-gray-600'></div>
<div className='absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform peer-checked:translate-x-5'></div>
</div>
</label>
</div>
2025-08-12 21:50:58 +08:00
2026-01-04 16:08:27 +08:00
{/* 优选和测速 */}
<div className='flex items-center justify-between'>
<div>
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300'>
</h4>
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
</p>
</div>
<label className='flex items-center cursor-pointer'>
<div className='relative'>
<input
type='checkbox'
className='sr-only peer'
checked={enableOptimization}
onChange={(e) => handleOptimizationToggle(e.target.checked)}
/>
<div className='w-11 h-6 bg-gray-300 rounded-full peer-checked:bg-green-500 transition-colors dark:bg-gray-600'></div>
<div className='absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform peer-checked:translate-x-5'></div>
</div>
</label>
</div>
2025-08-17 17:32:42 +08:00
2026-01-04 16:08:27 +08:00
{/* 流式搜索 */}
<div className='flex items-center justify-between'>
<div>
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300'>
</h4>
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
使
</p>
</div>
<label className='flex items-center cursor-pointer'>
<div className='relative'>
<input
type='checkbox'
className='sr-only peer'
checked={fluidSearch}
onChange={(e) => handleFluidSearchToggle(e.target.checked)}
/>
<div className='w-11 h-6 bg-gray-300 rounded-full peer-checked:bg-green-500 transition-colors dark:bg-gray-600'></div>
<div className='absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform peer-checked:translate-x-5'></div>
</div>
</label>
</div>
2025-08-24 00:26:48 +08:00
2026-01-04 16:08:27 +08:00
{/* 直播视频浏览器直连 */}
<div className='flex items-center justify-between'>
<div>
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300'>
IPTV
</h4>
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
IPTV Allow CORS
</p>
</div>
<label className='flex items-center cursor-pointer'>
<div className='relative'>
<input
type='checkbox'
className='sr-only peer'
checked={liveDirectConnect}
onChange={(e) => handleLiveDirectConnectToggle(e.target.checked)}
/>
<div className='w-11 h-6 bg-gray-300 rounded-full peer-checked:bg-green-500 transition-colors dark:bg-gray-600'></div>
<div className='absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform peer-checked:translate-x-5'></div>
</div>
</label>
</div>
2025-12-05 20:05:30 +08:00
2026-01-04 16:08:27 +08:00
{/* 禁用背景图渲染 */}
<div className='flex items-center justify-between'>
<div>
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300'>
</h4>
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
TMDB背景图显示
</p>
</div>
<label className='flex items-center cursor-pointer'>
<div className='relative'>
<input
type='checkbox'
className='sr-only peer'
checked={tmdbBackdropDisabled}
onChange={(e) => handleTmdbBackdropDisabledToggle(e.target.checked)}
/>
<div className='w-11 h-6 bg-gray-300 rounded-full peer-checked:bg-green-500 transition-colors dark:bg-gray-600'></div>
<div className='absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform peer-checked:translate-x-5'></div>
</div>
</label>
</div>
2026-01-02 01:50:16 +08:00
2026-01-04 16:08:27 +08:00
{/* 启用预告片 */}
<div className='flex items-center justify-between'>
<div>
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300'>
</h4>
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
</p>
</div>
<label className='flex items-center cursor-pointer'>
<div className='relative'>
<input
type='checkbox'
className='sr-only peer'
checked={enableTrailers}
onChange={(e) => handleEnableTrailersToggle(e.target.checked)}
/>
<div className='w-11 h-6 bg-gray-300 rounded-full peer-checked:bg-green-500 transition-colors dark:bg-gray-600'></div>
<div className='absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform peer-checked:translate-x-5'></div>
</div>
</label>
</div>
2026-01-12 23:28:48 +08:00
{/* 搜索繁体转简体 */}
<div className='flex items-center justify-between'>
<div>
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300'>
</h4>
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
</p>
</div>
<label className='flex items-center cursor-pointer'>
<div className='relative'>
<input
type='checkbox'
className='sr-only peer'
checked={searchTraditionalToSimplified}
onChange={(e) => handleSearchTraditionalToSimplifiedToggle(e.target.checked)}
/>
<div className='w-11 h-6 bg-gray-300 rounded-full peer-checked:bg-green-500 transition-colors dark:bg-gray-600'></div>
<div className='absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform peer-checked:translate-x-5'></div>
</div>
</label>
</div>
2025-12-24 21:55:27 +08:00
</div>
2026-01-04 16:08:27 +08:00
)}
2025-12-24 21:55:27 +08:00
</div>
2025-12-28 22:11:26 +08:00
{/* 缓冲设置 */}
2026-01-04 16:08:27 +08:00
<div className='border border-gray-200 dark:border-gray-700 rounded-lg overflow-visible'>
<button
onClick={() => setIsBufferSectionOpen(!isBufferSectionOpen)}
className='w-full px-3 py-2.5 md:px-4 md:py-3 bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-750 transition-colors flex items-center justify-between'
>
<h3 className='text-base font-semibold text-gray-800 dark:text-gray-200'>
2025-12-28 22:11:26 +08:00
2026-01-04 16:08:27 +08:00
</h3>
{isBufferSectionOpen ? (
<ChevronUp className='w-5 h-5 text-gray-600 dark:text-gray-400' />
) : (
<ChevronDown className='w-5 h-5 text-gray-600 dark:text-gray-400' />
)}
</button>
{isBufferSectionOpen && (
<div className='p-3 md:p-4 space-y-4 md:space-y-6'>
<div>
<p className='text-xs text-gray-500 dark:text-gray-400'>
</p>
2025-12-28 22:11:26 +08:00
</div>
2026-01-04 16:08:27 +08:00
{/* 缓冲策略 */}
<div className='space-y-3'>
<div>
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300'>
</h4>
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
</p>
</div>
2026-01-15 12:43:40 +08:00
{/* 滑块控件 */}
<div className='space-y-2'>
<input
type='range'
min='0'
max='3'
step='1'
value={getSliderValueFromStrategy(bufferStrategy)}
onChange={(e) => {
const sliderValue = parseInt(e.target.value);
const strategy = getBufferStrategyFromSlider(sliderValue);
handleBufferStrategyChange(strategy);
}}
className='w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-green-500'
style={{
background: `linear-gradient(to right, rgb(34 197 94) 0%, rgb(34 197 94) ${(getSliderValueFromStrategy(bufferStrategy) / 3) * 100}%, rgb(229 231 235) ${(getSliderValueFromStrategy(bufferStrategy) / 3) * 100}%, rgb(229 231 235) 100%)`
}}
/>
{/* 标签显示 */}
<div className='flex justify-between text-xs text-gray-500 dark:text-gray-400 px-1'>
<span className={bufferStrategy === 'low' ? 'font-semibold text-green-600 dark:text-green-400' : ''}>
</span>
<span className={bufferStrategy === 'medium' ? 'font-semibold text-green-600 dark:text-green-400' : ''}>
</span>
<span className={bufferStrategy === 'high' ? 'font-semibold text-green-600 dark:text-green-400' : ''}>
</span>
<span className={bufferStrategy === 'ultra' ? 'font-semibold text-green-600 dark:text-green-400' : ''}>
</span>
</div>
{/* 当前选择的说明 */}
<div className='text-center text-sm font-medium text-gray-700 dark:text-gray-300 mt-2'>
2026-01-04 16:08:27 +08:00
{
bufferStrategyOptions.find(
(option) => option.value === bufferStrategy
)?.label
}
</div>
2025-12-28 22:11:26 +08:00
</div>
2026-01-04 16:08:27 +08:00
</div>
2025-12-28 22:11:26 +08:00
2026-01-04 16:08:27 +08:00
{/* 下集预缓冲 */}
<div className='flex items-center justify-between'>
<div>
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300'>
</h4>
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
90%
</p>
</div>
<label className='flex items-center cursor-pointer'>
<div className='relative'>
<input
type='checkbox'
className='sr-only peer'
checked={nextEpisodePreCache}
onChange={(e) => handleNextEpisodePreCacheToggle(e.target.checked)}
/>
<div className='w-11 h-6 bg-gray-300 rounded-full peer-checked:bg-green-500 transition-colors dark:bg-gray-600'></div>
<div className='absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform peer-checked:translate-x-5'></div>
</div>
</label>
2025-12-28 22:11:26 +08:00
</div>
2026-01-04 16:08:27 +08:00
</div>
)}
2025-12-28 22:11:26 +08:00
</div>
2026-01-04 16:08:27 +08:00
{/* 弹幕设置 */}
<div className='border border-gray-200 dark:border-gray-700 rounded-lg overflow-visible'>
2025-12-05 20:05:30 +08:00
<button
2026-01-04 16:08:27 +08:00
onClick={() => setIsDanmakuSectionOpen(!isDanmakuSectionOpen)}
className='w-full px-3 py-2.5 md:px-4 md:py-3 bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-750 transition-colors flex items-center justify-between'
2025-12-05 20:05:30 +08:00
>
2026-01-04 16:08:27 +08:00
<h3 className='text-base font-semibold text-gray-800 dark:text-gray-200'>
</h3>
{isDanmakuSectionOpen ? (
<ChevronUp className='w-5 h-5 text-gray-600 dark:text-gray-400' />
2025-12-05 20:05:30 +08:00
) : (
2026-01-04 16:08:27 +08:00
<ChevronDown className='w-5 h-5 text-gray-600 dark:text-gray-400' />
2025-12-05 20:05:30 +08:00
)}
</button>
2026-01-04 16:08:27 +08:00
{isDanmakuSectionOpen && (
<div className='p-3 md:p-4 space-y-4 md:space-y-6'>
2026-01-13 14:45:22 +08:00
{/* 下集弹幕预加载 */}
<div className='flex items-center justify-between'>
<div>
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300'>
</h4>
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
90%
</p>
</div>
<label className='flex items-center cursor-pointer'>
<div className='relative'>
<input
type='checkbox'
className='sr-only peer'
checked={nextEpisodeDanmakuPreload}
onChange={(e) => handleNextEpisodeDanmakuPreloadToggle(e.target.checked)}
/>
<div className='w-11 h-6 bg-gray-300 rounded-full peer-checked:bg-green-500 transition-colors dark:bg-gray-600'></div>
<div className='absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform peer-checked:translate-x-5'></div>
</div>
</label>
</div>
2026-01-04 16:08:27 +08:00
{/* 清除弹幕缓存 */}
<div className='space-y-3'>
<div>
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300'>
</h4>
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
</p>
</div>
<button
onClick={handleClearDanmakuCache}
disabled={isClearingCache}
className='w-full px-4 py-2.5 bg-red-500 hover:bg-red-600 disabled:bg-red-400 dark:bg-red-600 dark:hover:bg-red-700 dark:disabled:bg-red-500 text-white text-sm font-medium rounded-lg transition-colors duration-200 shadow-sm hover:shadow-md disabled:cursor-not-allowed flex items-center justify-center gap-2'
>
{isClearingCache ? (
<>
<div className='w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin'></div>
<span>...</span>
</>
) : (
<>
<svg className='w-4 h-4' fill='none' stroke='currentColor' viewBox='0 0 24 24'>
<path strokeLinecap='round' strokeLinejoin='round' strokeWidth={2} d='M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16' />
</svg>
<span></span>
</>
)}
</button>
{/* 成功/失败提示 */}
{clearCacheMessage && (
<div className={`text-sm p-3 rounded-lg border ${
clearCacheMessage.includes('成功')
? 'bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800 text-green-700 dark:text-green-300'
: 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800 text-red-700 dark:text-red-300'
}`}>
{clearCacheMessage}
</div>
)}
</div>
2025-12-05 20:05:30 +08:00
</div>
)}
</div>
2026-01-04 16:29:16 +08:00
{/* 首页设置 */}
<div className='border border-gray-200 dark:border-gray-700 rounded-lg overflow-visible'>
<button
onClick={() => setIsHomepageSectionOpen(!isHomepageSectionOpen)}
className='w-full px-3 py-2.5 md:px-4 md:py-3 bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-750 transition-colors flex items-center justify-between'
>
<h3 className='text-base font-semibold text-gray-800 dark:text-gray-200'>
</h3>
{isHomepageSectionOpen ? (
<ChevronUp className='w-5 h-5 text-gray-600 dark:text-gray-400' />
) : (
<ChevronDown className='w-5 h-5 text-gray-600 dark:text-gray-400' />
)}
</button>
{isHomepageSectionOpen && (
<div className='p-3 md:p-4 space-y-4 md:space-y-6'>
<div>
<p className='text-xs text-gray-500 dark:text-gray-400 mb-3'>
</p>
</div>
{/* 模块列表 */}
<div className='space-y-2'>
{homeModules.map((module, index) => (
<div
key={module.id}
className='flex items-center gap-2 p-3 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700'
>
{/* 左侧:显示/隐藏开关 */}
<button
onClick={() => handleHomeModuleToggle(module.id, !module.enabled)}
className='flex-shrink-0'
title={module.enabled ? '点击隐藏' : '点击显示'}
>
{module.enabled ? (
<Eye className='w-5 h-5 text-green-600 dark:text-green-400' />
) : (
<EyeOff className='w-5 h-5 text-gray-400 dark:text-gray-500' />
)}
</button>
{/* 中间:模块名称 */}
<div className='flex-1'>
<span className={`text-sm font-medium ${
module.enabled
? 'text-gray-900 dark:text-gray-100'
: 'text-gray-400 dark:text-gray-500'
}`}>
{module.name}
</span>
</div>
{/* 右侧:上下移动按钮 */}
<div className='flex gap-1'>
<button
onClick={() => handleHomeModuleMoveUp(index)}
disabled={index === 0}
className='p-1.5 rounded hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-30 disabled:cursor-not-allowed transition-colors'
title='上移'
>
<MoveUp className='w-4 h-4 text-gray-600 dark:text-gray-400' />
</button>
<button
onClick={() => handleHomeModuleMoveDown(index)}
disabled={index === homeModules.length - 1}
className='p-1.5 rounded hover:bg-gray-200 dark:hover:bg-gray-700 disabled:opacity-30 disabled:cursor-not-allowed transition-colors'
title='下移'
>
<MoveDown className='w-4 h-4 text-gray-600 dark:text-gray-400' />
</button>
</div>
</div>
))}
</div>
{/* 恢复默认按钮 */}
<button
onClick={() => {
setHomeModules(defaultHomeModules);
if (typeof window !== 'undefined') {
localStorage.setItem('homeModules', JSON.stringify(defaultHomeModules));
window.dispatchEvent(new CustomEvent('homeModulesUpdated'));
}
}}
className='w-full px-4 py-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300 text-sm font-medium rounded-lg transition-colors'
>
</button>
{/* 提示信息 */}
<div className='text-xs text-gray-500 dark:text-gray-400 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg'>
<p>💡 /使</p>
</div>
</div>
)}
</div>
2025-08-17 17:32:42 +08:00
</div>
2025-08-12 21:50:58 +08:00
2025-08-17 20:34:47 +08:00
{/* 底部说明 */}
<div className='mt-6 pt-4 border-t border-gray-200 dark:border-gray-700'>
<p className='text-xs text-gray-500 dark:text-gray-400 text-center'>
</p>
</div>
2025-08-12 21:50:58 +08:00
</div>
</div>
</>
);
// 订阅面板内容
const subscribePanel = (
<>
{/* 背景遮罩 */}
<div
className='fixed inset-0 bg-black/50 backdrop-blur-sm z-[1000]'
onClick={handleCloseSubscribe}
onTouchMove={(e) => {
e.preventDefault();
}}
onWheel={(e) => {
e.preventDefault();
}}
style={{
touchAction: 'none',
}}
/>
{/* 订阅面板 */}
<div
className='fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-md bg-white dark:bg-gray-900 rounded-xl shadow-xl z-[1001] overflow-hidden'
>
<div
className='h-full p-6'
data-panel-content
onTouchMove={(e) => {
e.stopPropagation();
}}
style={{
touchAction: 'auto',
}}
>
{/* 标题栏 */}
<div className='flex items-center justify-between mb-6'>
<h3 className='text-xl font-bold text-gray-800 dark:text-gray-200'>
</h3>
<button
onClick={handleCloseSubscribe}
className='w-8 h-8 p-1 rounded-full flex items-center justify-center text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors'
aria-label='Close'
>
<X className='w-full h-full' />
</button>
</div>
{/* 内容 */}
<div className='space-y-4'>
{/* 去广告开关 */}
<div className='flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-800 rounded-lg'>
<div>
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300'>
广
</h4>
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
广
</p>
</div>
<label className='flex items-center cursor-pointer'>
<div className='relative'>
<input
type='checkbox'
className='sr-only peer'
checked={adFilterEnabled}
2025-12-09 21:33:15 +08:00
onChange={(e) => handleAdFilterToggle(e.target.checked)}
/>
<div className='w-11 h-6 bg-gray-300 rounded-full peer-checked:bg-green-500 transition-colors dark:bg-gray-600'></div>
<div className='absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full transition-transform peer-checked:translate-x-5'></div>
</div>
</label>
</div>
{/* TVBOX订阅 */}
<div>
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
TVBOX订阅
</h4>
<div className='flex gap-2'>
<input
type='text'
className='flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-sm bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100 cursor-not-allowed'
value={subscribeUrl}
disabled
readOnly
/>
<button
onClick={handleCopySubscribeUrl}
className='px-4 py-2 bg-green-600 hover:bg-green-700 dark:bg-green-700 dark:hover:bg-green-600 text-white text-sm font-medium rounded-md transition-colors flex items-center gap-2'
>
<Copy className='w-4 h-4' />
{copySuccess ? '已复制' : '复制'}
</button>
</div>
</div>
</div>
{/* 底部说明 */}
<div className='mt-6 pt-4 border-t border-gray-200 dark:border-gray-700'>
<p className='text-xs text-gray-500 dark:text-gray-400 text-center'>
TVBOX应用中使用
</p>
</div>
</div>
</div>
</>
);
2025-08-12 21:50:58 +08:00
// 修改密码面板内容
const changePasswordPanel = (
<>
{/* 背景遮罩 */}
<div
className='fixed inset-0 bg-black/50 backdrop-blur-sm z-[1000]'
onClick={handleCloseChangePassword}
2025-08-17 20:34:47 +08:00
onTouchMove={(e) => {
// 只阻止滚动,允许其他触摸事件
e.preventDefault();
}}
onWheel={(e) => {
// 阻止滚轮滚动
e.preventDefault();
}}
style={{
touchAction: 'none',
}}
2025-08-12 21:50:58 +08:00
/>
{/* 修改密码面板 */}
2025-08-17 20:34:47 +08:00
<div
className='fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-md bg-white dark:bg-gray-900 rounded-xl shadow-xl z-[1001] overflow-hidden'
>
{/* 内容容器 - 独立的滚动区域 */}
<div
className='h-full p-6'
data-panel-content
onTouchMove={(e) => {
// 阻止事件冒泡到遮罩层,但允许内部滚动
e.stopPropagation();
}}
style={{
touchAction: 'auto', // 允许所有触摸操作
}}
>
{/* 标题栏 */}
<div className='flex items-center justify-between mb-6'>
<h3 className='text-xl font-bold text-gray-800 dark:text-gray-200'>
</h3>
<button
onClick={handleCloseChangePassword}
className='w-8 h-8 p-1 rounded-full flex items-center justify-center text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors'
aria-label='Close'
>
<X className='w-full h-full' />
</button>
2025-08-12 21:50:58 +08:00
</div>
2025-08-17 20:34:47 +08:00
{/* 表单 */}
<div className='space-y-4'>
{/* 新密码输入 */}
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
</label>
<input
type='password'
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent transition-colors bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400'
placeholder='请输入新密码'
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
disabled={passwordLoading}
/>
</div>
2025-08-12 21:50:58 +08:00
2025-08-17 20:34:47 +08:00
{/* 确认密码输入 */}
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
</label>
<input
type='password'
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent transition-colors bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400'
placeholder='请再次输入新密码'
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
disabled={passwordLoading}
/>
2025-08-12 21:50:58 +08:00
</div>
2025-08-17 20:34:47 +08:00
{/* 错误信息 */}
{passwordError && (
<div className='text-red-500 text-sm bg-red-50 dark:bg-red-900/20 p-3 rounded-md border border-red-200 dark:border-red-800'>
{passwordError}
</div>
)}
</div>
2025-08-12 21:50:58 +08:00
2025-08-17 20:34:47 +08:00
{/* 操作按钮 */}
<div className='flex gap-3 mt-6 pt-4 border-t border-gray-200 dark:border-gray-700'>
<button
onClick={handleCloseChangePassword}
className='flex-1 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md transition-colors'
disabled={passwordLoading}
>
</button>
<button
onClick={handleSubmitChangePassword}
className='flex-1 px-4 py-2 text-sm font-medium text-white bg-green-600 hover:bg-green-700 dark:bg-green-700 dark:hover:bg-green-600 rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed'
disabled={passwordLoading || !newPassword || !confirmPassword}
>
{passwordLoading ? '修改中...' : '确认修改'}
</button>
</div>
{/* 底部说明 */}
<div className='mt-4 pt-4 border-t border-gray-200 dark:border-gray-700'>
<p className='text-xs text-gray-500 dark:text-gray-400 text-center'>
</p>
</div>
2025-08-12 21:50:58 +08:00
</div>
</div>
</>
);
2026-01-16 16:11:31 +08:00
// 邮件设置面板内容
const emailSettingsPanel = (
<>
{/* 背景遮罩 */}
<div
className='fixed inset-0 bg-black/50 backdrop-blur-sm z-[1000]'
onClick={() => setIsEmailSettingsOpen(false)}
onTouchMove={(e) => {
e.preventDefault();
}}
onWheel={(e) => {
e.preventDefault();
}}
style={{
touchAction: 'none',
}}
/>
{/* 邮件设置面板 */}
<div
className='fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-md bg-white dark:bg-gray-900 rounded-xl shadow-xl z-[1001] overflow-hidden'
>
<div
className='h-full p-6'
data-panel-content
onTouchMove={(e) => {
e.stopPropagation();
}}
style={{
touchAction: 'auto',
}}
>
{/* 标题栏 */}
<div className='flex items-center justify-between mb-6'>
<h3 className='text-xl font-bold text-gray-800 dark:text-gray-200'>
</h3>
<button
onClick={() => setIsEmailSettingsOpen(false)}
className='w-8 h-8 p-1 rounded-full flex items-center justify-center text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors'
aria-label='Close'
>
<X className='w-full h-full' />
</button>
</div>
{/* 表单 */}
{emailSettingsLoading ? (
<div className='space-y-4'>
{/* 加载骨架屏 */}
<div className='animate-pulse'>
<div className='h-4 bg-gray-200 dark:bg-gray-700 rounded w-20 mb-2'></div>
<div className='h-10 bg-gray-200 dark:bg-gray-700 rounded'></div>
</div>
<div className='animate-pulse'>
<div className='h-20 bg-gray-200 dark:bg-gray-700 rounded'></div>
</div>
<div className='animate-pulse'>
<div className='h-10 bg-gray-200 dark:bg-gray-700 rounded'></div>
</div>
<div className='text-center text-sm text-gray-500 dark:text-gray-400'>
...
</div>
2026-01-16 16:11:31 +08:00
</div>
) : (
<div className='space-y-4'>
2026-01-16 16:11:31 +08:00
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
</label>
<input
type='email'
value={userEmail}
onChange={(e) => setUserEmail(e.target.value)}
placeholder='输入您的邮箱地址'
disabled={emailSettingsSaving}
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed'
/>
2026-01-16 16:11:31 +08:00
</div>
<div className='flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-800 rounded-lg'>
<div>
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300'>
</h4>
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
</p>
</div>
<button
onClick={() => setEmailNotifications(!emailNotifications)}
disabled={emailSettingsSaving}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors disabled:opacity-50 disabled:cursor-not-allowed ${
emailNotifications ? 'bg-blue-600' : 'bg-gray-200 dark:bg-gray-700'
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
emailNotifications ? 'translate-x-6' : 'translate-x-1'
}`}
/>
</button>
</div>
2026-01-16 16:11:31 +08:00
<button
onClick={handleSaveEmailSettings}
disabled={emailSettingsSaving}
className='w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 dark:disabled:bg-blue-500 text-white text-sm font-medium rounded-lg transition-colors disabled:cursor-not-allowed flex items-center justify-center gap-2'
2026-01-16 16:11:31 +08:00
>
{emailSettingsSaving ? (
<>
<div className='w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin'></div>
<span>...</span>
</>
) : (
'保存设置'
)}
2026-01-16 16:11:31 +08:00
</button>
<p id='email-settings-message' className='text-xs text-center hidden'></p>
</div>
)}
2026-01-16 16:11:31 +08:00
{/* 提示信息 */}
<div className='mt-6 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg'>
<p className='text-xs text-blue-800 dark:text-blue-200'>
💡
</p>
</div>
</div>
</div>
</>
);
2025-08-12 21:50:58 +08:00
return (
<>
<div className='relative'>
<button
onClick={handleMenuClick}
className='w-10 h-10 p-2 rounded-full flex items-center justify-center text-gray-600 hover:bg-gray-200/50 dark:text-gray-300 dark:hover:bg-gray-700/50 transition-colors'
aria-label='User Menu'
>
<User className='w-full h-full' />
</button>
2025-12-18 23:31:10 +08:00
{/* 版本更新红点 */}
2025-08-12 21:50:58 +08:00
{updateStatus === UpdateStatus.HAS_UPDATE && (
<div className='absolute top-[2px] right-[2px] w-2 h-2 bg-yellow-500 rounded-full'></div>
)}
2025-12-18 23:31:10 +08:00
{/* 未读通知红点 */}
{unreadCount > 0 && (
<div className='absolute top-[2px] right-[2px] w-2 h-2 bg-red-500 rounded-full'></div>
)}
2025-08-12 21:50:58 +08:00
</div>
{/* 使用 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)}
{/* 使用 Portal 将订阅面板渲染到 document.body */}
{isSubscribeOpen &&
mounted &&
createPortal(subscribePanel, document.body)}
2025-08-12 21:50:58 +08:00
{/* 版本面板 */}
<VersionPanel
isOpen={isVersionPanelOpen}
onClose={() => setIsVersionPanelOpen(false)}
/>
2025-12-14 00:22:38 +08:00
{/* 离线下载面板 */}
<OfflineDownloadPanel
isOpen={isOfflineDownloadPanelOpen}
onClose={() => setIsOfflineDownloadPanelOpen(false)}
/>
2025-12-18 23:31:10 +08:00
{/* 使用 Portal 将通知面板渲染到 document.body */}
{isNotificationPanelOpen &&
mounted &&
createPortal(
<NotificationPanel
isOpen={isNotificationPanelOpen}
onClose={() => {
setIsNotificationPanelOpen(false);
2025-12-18 23:48:12 +08:00
// 不需要在这里刷新NotificationPanel 内部会触发事件
2025-12-18 23:31:10 +08:00
}}
/>,
document.body
)}
{/* 使用 Portal 将收藏面板渲染到 document.body */}
{isFavoritesPanelOpen &&
mounted &&
createPortal(
<FavoritesPanel
isOpen={isFavoritesPanelOpen}
onClose={() => setIsFavoritesPanelOpen(false)}
/>,
document.body
)}
2026-01-16 16:11:31 +08:00
{/* 使用 Portal 将邮件设置面板渲染到 document.body */}
{isEmailSettingsOpen &&
mounted &&
createPortal(emailSettingsPanel, document.body)}
2025-08-12 21:50:58 +08:00
</>
);
};