feat: 添加成人内容过滤功能

- 新增用户设置系统支持内容过滤开关
- 扩展类型定义支持成人内容标记
- 实现用户设置API端点(GET/PATCH/PUT)
- 增强搜索API支持内容分组和过滤
- 创建AdultContentFilter UI组件
- 添加用户设置页面和认证检查
- 更新配置示例和README文档
- 实现LocalStorage和Redis存储后端
- 默认启用过滤确保安全性
This commit is contained in:
katelya
2025-09-04 21:11:02 +08:00
parent c9429efba6
commit 86ebbb2cf6
12 changed files with 658 additions and 17 deletions
+163
View File
@@ -0,0 +1,163 @@
'use client';
import { useEffect, useState } from 'react';
import { Shield, ShieldOff } from 'lucide-react';
interface AdultContentFilterProps {
userName: string;
onUpdate?: (enabled: boolean) => void;
}
const AdultContentFilter: React.FC<AdultContentFilterProps> = ({
userName,
onUpdate
}) => {
const [isEnabled, setIsEnabled] = useState(true); // 默认开启过滤
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// 获取用户设置
useEffect(() => {
const fetchUserSettings = async () => {
if (!userName) return;
try {
const response = await fetch('/api/user/settings', {
headers: {
'Authorization': `Bearer ${userName}`,
},
});
if (response.ok) {
const data = await response.json();
setIsEnabled(data.settings.filter_adult_content);
} else {
setError('获取用户设置失败');
}
} catch (err) {
setError('网络连接失败');
// eslint-disable-next-line no-console
console.error('Failed to fetch user settings:', err);
}
};
fetchUserSettings();
}, [userName]);
// 更新用户设置
const handleToggle = async () => {
if (!userName || isLoading) return;
setIsLoading(true);
setError(null);
try {
const response = await fetch('/api/user/settings', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${userName}`,
},
body: JSON.stringify({
settings: {
filter_adult_content: !isEnabled,
},
}),
});
if (response.ok) {
const newState = !isEnabled;
setIsEnabled(newState);
onUpdate?.(newState);
} else {
const errorData = await response.json();
setError(errorData.error || '更新设置失败');
}
} catch (err) {
setError('网络连接失败');
// eslint-disable-next-line no-console
console.error('Failed to update user settings:', err);
} finally {
setIsLoading(false);
}
};
return (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<div className="flex items-start justify-between">
<div className="flex items-center space-x-3">
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-blue-100 dark:bg-blue-900">
{isEnabled ? (
<Shield className="w-5 h-5 text-blue-600 dark:text-blue-400" />
) : (
<ShieldOff className="w-5 h-5 text-gray-600 dark:text-gray-400" />
)}
</div>
<div className="flex-1">
<h3 className="text-lg font-medium text-gray-900 dark:text-white">
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
{isEnabled
? '已开启过滤,将自动隐藏所有标记为"成人"的资源站及其内容'
: '已关闭过滤,成人内容将在搜索结果中单独分组显示'
}
</p>
</div>
</div>
<div className="flex items-center space-x-3">
<button
onClick={handleToggle}
disabled={isLoading || !userName}
className={`
relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-white dark:focus:ring-offset-gray-800 disabled:opacity-50 disabled:cursor-not-allowed
${isEnabled
? '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
${isEnabled ? 'translate-x-6' : 'translate-x-1'}
`}
/>
</button>
{isLoading && (
<div className="w-5 h-5">
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-blue-600"></div>
</div>
)}
</div>
</div>
{error && (
<div className="mt-4 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-md">
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
</div>
)}
<div className="mt-4 p-4 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-md">
<div className="flex items-start">
<div className="flex-shrink-0">
<Shield className="w-5 h-5 text-amber-600 dark:text-amber-400" />
</div>
<div className="ml-3">
<h4 className="text-sm font-medium text-amber-800 dark:text-amber-200">
</h4>
<p className="mt-1 text-sm text-amber-700 dark:text-amber-300">
使访18
</p>
</div>
</div>
</div>
</div>
);
};
export default AdultContentFilter;