feat: add global douban proxy config

This commit is contained in:
shinya
2025-07-26 15:39:29 +08:00
parent 090d10e4bb
commit 3de3bd2f39
8 changed files with 155 additions and 17 deletions

View File

@@ -51,6 +51,7 @@ interface SiteConfig {
SearchDownstreamMaxPage: number;
SiteInterfaceCacheTime: number;
ImageProxy: string;
DoubanProxy: string;
}
// 视频源数据类型
@@ -962,6 +963,7 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
SearchDownstreamMaxPage: 1,
SiteInterfaceCacheTime: 7200,
ImageProxy: '',
DoubanProxy: '',
});
// 保存状态
const [saving, setSaving] = useState(false);
@@ -979,6 +981,7 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
setSiteSettings({
...config.SiteConfig,
ImageProxy: config.SiteConfig.ImageProxy || '',
DoubanProxy: config.SiteConfig.DoubanProxy || '',
});
}
}, [config]);
@@ -1172,6 +1175,39 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
</p>
</div>
{/* 豆瓣代理设置 */}
<div>
<label
className={`block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 ${
isD1Storage || isUpstashStorage ? 'opacity-50' : ''
}`}
>
</label>
<input
type='text'
placeholder='例如: https://proxy.example.com/fetch?url='
value={siteSettings.DoubanProxy}
onChange={(e) =>
!isD1Storage &&
!isUpstashStorage &&
setSiteSettings((prev) => ({
...prev,
DoubanProxy: e.target.value,
}))
}
disabled={isD1Storage || isUpstashStorage}
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-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent ${
isD1Storage || isUpstashStorage
? 'opacity-50 cursor-not-allowed'
: ''
}`}
/>
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
访访使API
</p>
</div>
{/* 操作按钮 */}
<div className='flex justify-end'>
<button

View File

@@ -34,12 +34,14 @@ export async function POST(request: NextRequest) {
SearchDownstreamMaxPage,
SiteInterfaceCacheTime,
ImageProxy,
DoubanProxy,
} = body as {
SiteName: string;
Announcement: string;
SearchDownstreamMaxPage: number;
SiteInterfaceCacheTime: number;
ImageProxy: string;
DoubanProxy: string;
};
// 参数校验
@@ -48,7 +50,8 @@ export async function POST(request: NextRequest) {
typeof Announcement !== 'string' ||
typeof SearchDownstreamMaxPage !== 'number' ||
typeof SiteInterfaceCacheTime !== 'number' ||
typeof ImageProxy !== 'string'
typeof ImageProxy !== 'string' ||
typeof DoubanProxy !== 'string'
) {
return NextResponse.json({ error: '参数格式错误' }, { status: 400 });
}
@@ -74,6 +77,7 @@ export async function POST(request: NextRequest) {
SearchDownstreamMaxPage,
SiteInterfaceCacheTime,
ImageProxy,
DoubanProxy,
};
// 写入数据库

View File

@@ -44,6 +44,7 @@ export default async function RootLayout({
'本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。';
let enableRegister = process.env.NEXT_PUBLIC_ENABLE_REGISTER === 'true';
let imageProxy = process.env.NEXT_PUBLIC_IMAGE_PROXY || '';
let doubanProxy = process.env.NEXT_PUBLIC_DOUBAN_PROXY || '';
if (
process.env.NEXT_PUBLIC_STORAGE_TYPE !== 'd1' &&
process.env.NEXT_PUBLIC_STORAGE_TYPE !== 'upstash'
@@ -53,6 +54,7 @@ export default async function RootLayout({
announcement = config.SiteConfig.Announcement;
enableRegister = config.UserConfig.AllowRegister;
imageProxy = config.SiteConfig.ImageProxy;
doubanProxy = config.SiteConfig.DoubanProxy;
}
// 将运行时配置注入到全局 window 对象,供客户端在运行时读取
@@ -60,6 +62,7 @@ export default async function RootLayout({
STORAGE_TYPE: process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage',
ENABLE_REGISTER: enableRegister,
IMAGE_PROXY: imageProxy,
DOUBAN_PROXY: doubanProxy,
};
return (

View File

@@ -13,6 +13,7 @@ export const SettingsButton: React.FC = () => {
const [imageProxyUrl, setImageProxyUrl] = useState('');
const [enableOptimization, setEnableOptimization] = useState(true);
const [enableImageProxy, setEnableImageProxy] = useState(false);
const [enableDoubanProxy, setEnableDoubanProxy] = useState(false);
const [mounted, setMounted] = useState(false);
// 确保组件已挂载
@@ -30,9 +31,21 @@ export const SettingsButton: React.FC = () => {
setDefaultAggregateSearch(JSON.parse(savedAggregateSearch));
}
const savedEnableDoubanProxy = localStorage.getItem('enableDoubanProxy');
const defaultDoubanProxy =
(window as any).RUNTIME_CONFIG?.DOUBAN_PROXY || '';
if (savedEnableDoubanProxy !== null) {
setEnableDoubanProxy(JSON.parse(savedEnableDoubanProxy));
} else if (defaultDoubanProxy) {
// 如果有默认豆瓣代理配置,则默认开启
setEnableDoubanProxy(true);
}
const savedDoubanProxyUrl = localStorage.getItem('doubanProxyUrl');
if (savedDoubanProxyUrl !== null) {
setDoubanProxyUrl(savedDoubanProxyUrl);
} else if (defaultDoubanProxy) {
setDoubanProxyUrl(defaultDoubanProxy);
}
const savedEnableImageProxy = localStorage.getItem('enableImageProxy');
@@ -96,6 +109,13 @@ export const SettingsButton: React.FC = () => {
}
};
const handleDoubanProxyToggle = (value: boolean) => {
setEnableDoubanProxy(value);
if (typeof window !== 'undefined') {
localStorage.setItem('enableDoubanProxy', JSON.stringify(value));
}
};
const handleSettingsClick = () => {
setIsOpen(!isOpen);
};
@@ -107,11 +127,14 @@ export const SettingsButton: React.FC = () => {
// 重置所有设置为默认值
const handleResetSettings = () => {
const defaultImageProxy = (window as any).RUNTIME_CONFIG?.IMAGE_PROXY || '';
const defaultDoubanProxy =
(window as any).RUNTIME_CONFIG?.DOUBAN_PROXY || '';
// 重置所有状态
setDefaultAggregateSearch(true);
setEnableOptimization(true);
setDoubanProxyUrl('');
setDoubanProxyUrl(defaultDoubanProxy);
setEnableDoubanProxy(!!defaultDoubanProxy);
setEnableImageProxy(!!defaultImageProxy);
setImageProxyUrl(defaultImageProxy);
@@ -119,7 +142,11 @@ export const SettingsButton: React.FC = () => {
if (typeof window !== 'undefined') {
localStorage.setItem('defaultAggregateSearch', JSON.stringify(true));
localStorage.setItem('enableOptimization', JSON.stringify(true));
localStorage.setItem('doubanProxyUrl', '');
localStorage.setItem('doubanProxyUrl', defaultDoubanProxy);
localStorage.setItem(
'enableDoubanProxy',
JSON.stringify(!!defaultDoubanProxy)
);
localStorage.setItem(
'enableImageProxy',
JSON.stringify(!!defaultImageProxy)
@@ -212,25 +239,60 @@ export const SettingsButton: React.FC = () => {
</label>
</div>
{/* 豆瓣代理设置 */}
{/* 分割线 */}
<div className='border-t border-gray-200 dark:border-gray-700'></div>
{/* 豆瓣代理开关 */}
<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={enableDoubanProxy}
onChange={(e) => handleDoubanProxyToggle(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>
{/* 豆瓣代理地址设置 */}
<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'>
URL以绕过豆瓣访问限制使API
使 API
</p>
</div>
<input
type='text'
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-sm bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent'
className={`w-full px-3 py-2 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors ${
enableDoubanProxy
? 'border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400'
: 'border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50 text-gray-400 dark:text-gray-500 placeholder-gray-400 dark:placeholder-gray-600 cursor-not-allowed'
}`}
placeholder='例如: https://proxy.example.com/fetch?url='
value={doubanProxyUrl}
onChange={(e) => handleDoubanProxyUrlChange(e.target.value)}
disabled={!enableDoubanProxy}
/>
</div>
{/* 分割线 */}
<div className='border-t border-gray-200 dark:border-gray-700'></div>
{/* 图片代理开关 */}
<div className='flex items-center justify-between'>
<div>

View File

@@ -5,6 +5,7 @@ export interface AdminConfig {
SearchDownstreamMaxPage: number;
SiteInterfaceCacheTime: number;
ImageProxy: string;
DoubanProxy: string;
};
UserConfig: {
AllowRegister: boolean;

View File

@@ -159,6 +159,7 @@ async function initConfig() {
Number(process.env.NEXT_PUBLIC_SEARCH_MAX_PAGE) || 5,
SiteInterfaceCacheTime: fileConfig.cache_time || 7200,
ImageProxy: process.env.NEXT_PUBLIC_IMAGE_PROXY || '',
DoubanProxy: process.env.NEXT_PUBLIC_DOUBAN_PROXY || '',
},
UserConfig: {
AllowRegister: process.env.NEXT_PUBLIC_ENABLE_REGISTER === 'true',
@@ -197,6 +198,7 @@ async function initConfig() {
Number(process.env.NEXT_PUBLIC_SEARCH_MAX_PAGE) || 5,
SiteInterfaceCacheTime: fileConfig.cache_time || 7200,
ImageProxy: process.env.NEXT_PUBLIC_IMAGE_PROXY || '',
DoubanProxy: process.env.NEXT_PUBLIC_DOUBAN_PROXY || '',
},
UserConfig: {
AllowRegister: process.env.NEXT_PUBLIC_ENABLE_REGISTER === 'true',
@@ -341,6 +343,7 @@ export async function resetConfig() {
Number(process.env.NEXT_PUBLIC_SEARCH_MAX_PAGE) || 5,
SiteInterfaceCacheTime: fileConfig.cache_time || 7200,
ImageProxy: process.env.NEXT_PUBLIC_IMAGE_PROXY || '',
DoubanProxy: process.env.NEXT_PUBLIC_DOUBAN_PROXY || '',
},
UserConfig: {
AllowRegister: process.env.NEXT_PUBLIC_ENABLE_REGISTER === 'true',

View File

@@ -1,4 +1,5 @@
import { DoubanItem, DoubanResult } from './types';
import { getDoubanProxyUrl } from './utils';
interface DoubanCategoriesParams {
kind: 'tv' | 'movie';
@@ -60,16 +61,6 @@ async function fetchWithTimeout(
}
}
/**
* 获取豆瓣代理 URL 设置
*/
export function getDoubanProxyUrl(): string | null {
if (typeof window === 'undefined') return null;
const doubanProxyUrl = localStorage.getItem('doubanProxyUrl');
return doubanProxyUrl && doubanProxyUrl.trim() ? doubanProxyUrl.trim() : null;
}
/**
* 检查是否应该使用客户端获取豆瓣数据
*/

View File

@@ -40,6 +40,44 @@ export function processImageUrl(originalUrl: string): string {
return `${proxyUrl}${encodeURIComponent(originalUrl)}`;
}
/**
* 获取豆瓣代理 URL 设置
*/
export function getDoubanProxyUrl(): string | null {
if (typeof window === 'undefined') return null;
// 本地未开启豆瓣代理,则不使用代理
const enableDoubanProxy = localStorage.getItem('enableDoubanProxy');
if (enableDoubanProxy !== null) {
if (!JSON.parse(enableDoubanProxy) as boolean) {
return null;
}
}
const localDoubanProxy = localStorage.getItem('doubanProxyUrl');
if (localDoubanProxy != null) {
return localDoubanProxy.trim() ? localDoubanProxy.trim() : null;
}
// 如果未设置,则使用全局对象
const serverDoubanProxy = (window as any).RUNTIME_CONFIG?.DOUBAN_PROXY;
return serverDoubanProxy && serverDoubanProxy.trim()
? serverDoubanProxy.trim()
: null;
}
/**
* 处理豆瓣 URL如果设置了豆瓣代理则使用代理
*/
export function processDoubanUrl(originalUrl: string): string {
if (!originalUrl) return originalUrl;
const proxyUrl = getDoubanProxyUrl();
if (!proxyUrl) return originalUrl;
return `${proxyUrl}${encodeURIComponent(originalUrl)}`;
}
export function cleanHtmlTags(text: string): string {
if (!text) return '';
return text