/* eslint-disable @typescript-eslint/no-explicit-any */ 'use client'; import { Plus, ToggleLeft, ToggleRight,Trash2, X } from 'lucide-react'; import { useEffect, useRef,useState } from 'react'; import { getEpisodeFilterConfig, saveEpisodeFilterConfig } from '@/lib/db.client'; import { EpisodeFilterConfig, EpisodeFilterRule } from '@/lib/types'; interface EpisodeFilterSettingsProps { isOpen: boolean; onClose: () => void; onConfigUpdate?: (config: EpisodeFilterConfig) => void; onShowToast?: (message: string, type: 'success' | 'error' | 'info') => void; } export default function EpisodeFilterSettings({ isOpen, onClose, onConfigUpdate, onShowToast, }: EpisodeFilterSettingsProps) { const [config, setConfig] = useState({ rules: [] }); const [newKeyword, setNewKeyword] = useState(''); const [newType, setNewType] = useState<'normal' | 'regex'>('normal'); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); const [isVisible, setIsVisible] = useState(false); const [isAnimating, setIsAnimating] = useState(false); const [inputKey, setInputKey] = useState(0); // 用于强制重新渲染输入框 const inputRef = useRef(null); // 用于直接操作输入框 DOM // 控制动画状态 useEffect(() => { let animationId: number; let timer: NodeJS.Timeout; if (isOpen) { setIsVisible(true); // 使用双重 requestAnimationFrame 确保DOM完全渲染 animationId = requestAnimationFrame(() => { animationId = requestAnimationFrame(() => { setIsAnimating(true); }); }); } else { setIsAnimating(false); // 等待动画完成后隐藏组件 timer = setTimeout(() => { setIsVisible(false); }, 300); } return () => { if (animationId) { cancelAnimationFrame(animationId); } if (timer) { clearTimeout(timer); } }; }, [isOpen]); // 阻止背景滚动 useEffect(() => { if (isVisible) { // 保存当前滚动位置 const scrollY = window.scrollY; const scrollX = window.scrollX; const body = document.body; const html = document.documentElement; // 获取滚动条宽度 const scrollBarWidth = window.innerWidth - html.clientWidth; // 保存原始样式 const originalBodyStyle = { position: body.style.position, top: body.style.top, left: body.style.left, right: body.style.right, width: body.style.width, paddingRight: body.style.paddingRight, overflow: body.style.overflow, }; // 设置body样式来阻止滚动,但保持原位置 body.style.position = 'fixed'; body.style.top = `-${scrollY}px`; body.style.left = `-${scrollX}px`; body.style.right = '0'; body.style.width = '100%'; body.style.overflow = 'hidden'; body.style.paddingRight = `${scrollBarWidth}px`; return () => { // 恢复所有原始样式 body.style.position = originalBodyStyle.position; body.style.top = originalBodyStyle.top; body.style.left = originalBodyStyle.left; body.style.right = originalBodyStyle.right; body.style.width = originalBodyStyle.width; body.style.paddingRight = originalBodyStyle.paddingRight; body.style.overflow = originalBodyStyle.overflow; // 使用 requestAnimationFrame 确保样式恢复后再滚动 requestAnimationFrame(() => { window.scrollTo(scrollX, scrollY); }); }; } }, [isVisible]); // 加载配置 useEffect(() => { if (isOpen) { loadConfig(); } }, [isOpen]); const loadConfig = async () => { setLoading(true); try { const loadedConfig = await getEpisodeFilterConfig(); if (loadedConfig) { setConfig(loadedConfig); } else { setConfig({ rules: [] }); } } catch (error) { console.error('加载集数过滤配置失败:', error); } finally { setLoading(false); } }; // 保存配置 const handleSave = async () => { setSaving(true); try { await saveEpisodeFilterConfig(config); if (onConfigUpdate) { onConfigUpdate(config); } if (onShowToast) { onShowToast('保存成功!', 'success'); } // 延迟关闭面板,让用户看到toast setTimeout(() => { onClose(); }, 300); } catch (error) { console.error('保存集数过滤配置失败:', error); if (onShowToast) { onShowToast('保存失败,请重试', 'error'); } } finally { setSaving(false); } }; // 添加规则 const handleAddRule = () => { if (!newKeyword.trim()) { if (onShowToast) { onShowToast('请输入关键字', 'info'); } return; } const newRule: EpisodeFilterRule = { keyword: newKeyword.trim(), type: newType, enabled: true, id: Date.now().toString(), }; setConfig((prev) => ({ rules: [...prev.rules, newRule], })); // 清空输入框并强制重新渲染 setNewKeyword(''); // 使用 setTimeout 确保在状态更新后操作 DOM setTimeout(() => { if (inputRef.current) { inputRef.current.value = ''; // 直接清空 DOM 值 inputRef.current.blur(); // 失去焦点,阻止自动填充 } setInputKey(prev => prev + 1); // 强制重新渲染输入框 }, 0); }; // 删除规则 const handleDeleteRule = (id: string | undefined) => { if (!id) return; setConfig((prev) => ({ rules: prev.rules.filter((rule) => rule.id !== id), })); }; // 切换规则启用状态 const handleToggleRule = (id: string | undefined) => { if (!id) return; setConfig((prev) => ({ rules: prev.rules.map((rule) => rule.id === id ? { ...rule, enabled: !rule.enabled } : rule ), })); }; if (!isVisible) return null; return (
{ // 阻止最外层容器的触摸移动,防止背景滚动 e.preventDefault(); e.stopPropagation(); }} style={{ touchAction: 'none', // 禁用所有触摸操作 }} > {/* 背景遮罩 */}
{ // 只阻止滚动,允许其他触摸事件(包括点击) e.preventDefault(); }} onWheel={(e) => { // 阻止滚轮滚动 e.preventDefault(); }} style={{ backdropFilter: 'blur(4px)', willChange: 'opacity', touchAction: 'none', // 禁用所有触摸操作 }} /> {/* 弹窗主体 */}
{ // 允许弹窗内部滚动,阻止事件冒泡到外层 e.stopPropagation(); }} style={{ marginBottom: 'calc(0rem + env(safe-area-inset-bottom))', willChange: 'transform, opacity', backfaceVisibility: 'hidden', // 避免闪烁 transform: isAnimating ? 'translateY(0) translateZ(0)' : 'translateY(100%) translateZ(0)', // 组合变换保持滑入效果和硬件加速 opacity: isAnimating ? 1 : 0, touchAction: 'auto', // 允许弹窗内的正常触摸操作 }} > {/* 顶部拖拽指示器 */}
{/* 头部 */}

集数屏蔽设置

{/* 内容区域 */}
{/* 添加规则 */}

添加屏蔽规则

setNewKeyword(e.target.value)} onKeyPress={(e) => e.key === 'Enter' && handleAddRule()} placeholder="输入要屏蔽的集数关键字(如:预告、花絮)" autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck="false" data-form-type="other" data-lpignore="true" className="w-full px-4 py-3 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-lg border border-gray-200 dark:border-gray-600 focus:border-green-500 focus:outline-none focus:ring-2 focus:ring-green-500/20 transition-all duration-200" />

💡 普通模式:集数标题包含关键字即屏蔽
🔧 正则模式:支持正则表达式匹配(如:^预告.*匹配以"预告"开头的集数)

{/* 规则列表 */}

当前规则

{config.rules.length}
{loading ? (
加载中...
) : config.rules.length === 0 ? (

暂无屏蔽规则

点击上方添加关键字

) : (
{config.rules.map((rule) => (
{/* 启用/禁用按钮 */} {/* 关键字 */}
{rule.keyword} {rule.type === 'regex' ? '🔧 正则' : '💬 普通'}
{/* 删除按钮 */}
))}
)}
{/* 底部按钮 */}
); }