feat: add global image proxy config

This commit is contained in:
shinya
2025-07-15 22:20:42 +08:00
parent cca4092519
commit 90129c0d69
9 changed files with 165 additions and 16 deletions

View File

@@ -190,6 +190,7 @@ networks:
| REDIS_URL | redis 连接 url若 NEXT_PUBLIC_STORAGE_TYPE 为 redis 则必填 | 连接 url | 空 |
| NEXT_PUBLIC_ENABLE_REGISTER | 是否开放注册,仅在 redis 部署时生效 | true / false | false |
| NEXT_PUBLIC_SEARCH_MAX_PAGE | 搜索接口可拉取的最大页数 | 1-50 | 5 |
| NEXT_PUBLIC_IMAGE_PROXY | 默认的浏览器端图片代理 | url prefix | (空) |
## 配置说明

View File

@@ -50,6 +50,7 @@ interface SiteConfig {
Announcement: string;
SearchDownstreamMaxPage: number;
SiteInterfaceCacheTime: number;
ImageProxy: string;
}
// 视频源数据类型
@@ -947,6 +948,7 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
Announcement: '',
SearchDownstreamMaxPage: 1,
SiteInterfaceCacheTime: 7200,
ImageProxy: '',
});
// 保存状态
const [saving, setSaving] = useState(false);
@@ -958,7 +960,10 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
useEffect(() => {
if (config?.SiteConfig) {
setSiteSettings(config.SiteConfig);
setSiteSettings({
...config.SiteConfig,
ImageProxy: config.SiteConfig.ImageProxy || '',
});
}
}, [config]);
@@ -1092,6 +1097,41 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
/>
</div>
{/* 图片代理 */}
<div>
<label
className={`block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 ${
isD1Storage ? 'opacity-50' : ''
}`}
>
{isD1Storage && (
<span className='ml-2 text-xs text-gray-500 dark:text-gray-400'>
(D1 )
</span>
)}
</label>
<input
type='text'
placeholder='例如: https://imageproxy.example.com/?url='
value={siteSettings.ImageProxy}
onChange={(e) =>
!isD1Storage &&
setSiteSettings((prev) => ({
...prev,
ImageProxy: e.target.value,
}))
}
disabled={isD1Storage}
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 ? 'opacity-50 cursor-not-allowed' : ''
}`}
/>
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
访访使
</p>
</div>
{/* 操作按钮 */}
<div className='flex justify-end'>
<button

View File

@@ -33,11 +33,13 @@ export async function POST(request: NextRequest) {
Announcement,
SearchDownstreamMaxPage,
SiteInterfaceCacheTime,
ImageProxy,
} = body as {
SiteName: string;
Announcement: string;
SearchDownstreamMaxPage: number;
SiteInterfaceCacheTime: number;
ImageProxy: string;
};
// 参数校验
@@ -45,7 +47,8 @@ export async function POST(request: NextRequest) {
typeof SiteName !== 'string' ||
typeof Announcement !== 'string' ||
typeof SearchDownstreamMaxPage !== 'number' ||
typeof SiteInterfaceCacheTime !== 'number'
typeof SiteInterfaceCacheTime !== 'number' ||
typeof ImageProxy !== 'string'
) {
return NextResponse.json({ error: '参数格式错误' }, { status: 400 });
}
@@ -70,6 +73,7 @@ export async function POST(request: NextRequest) {
Announcement,
SearchDownstreamMaxPage,
SiteInterfaceCacheTime,
ImageProxy,
};
// 写入数据库

View File

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

View File

@@ -457,11 +457,11 @@ const EpisodeSelector: React.FC<EpisodeSelectorProps> = ({
onClick={() =>
!isCurrentSource && handleSourceClick(source)
}
className={`flex items-start gap-3 px-2 py-3 rounded-lg cursor-pointer transition-all duration-200 relative
className={`flex items-start gap-3 px-2 py-3 rounded-lg transition-all select-none duration-200 relative
${
isCurrentSource
? 'bg-green-500/10 dark:bg-green-500/20 border-green-500/30 border'
: 'hover:bg-gray-200/50 dark:hover:bg-white/10 hover:scale-[1.02]'
: 'hover:bg-gray-200/50 dark:hover:bg-white/10 hover:scale-[1.02] cursor-pointer'
}`.trim()}
>
{/* 封面 */}

View File

@@ -1,3 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client';
import { Settings, X } from 'lucide-react';
@@ -10,6 +12,7 @@ export const SettingsButton: React.FC = () => {
const [doubanProxyUrl, setDoubanProxyUrl] = useState('');
const [imageProxyUrl, setImageProxyUrl] = useState('');
const [enableOptimization, setEnableOptimization] = useState(true);
const [enableImageProxy, setEnableImageProxy] = useState(false);
const [mounted, setMounted] = useState(false);
// 确保组件已挂载
@@ -32,9 +35,21 @@ export const SettingsButton: React.FC = () => {
setDoubanProxyUrl(savedDoubanProxyUrl);
}
const savedEnableImageProxy = localStorage.getItem('enableImageProxy');
const defaultImageProxy =
(window as any).RUNTIME_CONFIG?.IMAGE_PROXY || '';
if (savedEnableImageProxy !== null) {
setEnableImageProxy(JSON.parse(savedEnableImageProxy));
} else if (defaultImageProxy) {
// 如果有默认图片代理配置,则默认开启
setEnableImageProxy(true);
}
const savedImageProxyUrl = localStorage.getItem('imageProxyUrl');
if (savedImageProxyUrl !== null) {
setImageProxyUrl(savedImageProxyUrl);
} else if (defaultImageProxy) {
setImageProxyUrl(defaultImageProxy);
}
const savedEnableOptimization =
@@ -74,6 +89,13 @@ export const SettingsButton: React.FC = () => {
}
};
const handleImageProxyToggle = (value: boolean) => {
setEnableImageProxy(value);
if (typeof window !== 'undefined') {
localStorage.setItem('enableImageProxy', JSON.stringify(value));
}
};
const handleSettingsClick = () => {
setIsOpen(!isOpen);
};
@@ -82,6 +104,30 @@ export const SettingsButton: React.FC = () => {
setIsOpen(false);
};
// 重置所有设置为默认值
const handleResetSettings = () => {
const defaultImageProxy = (window as any).RUNTIME_CONFIG?.IMAGE_PROXY || '';
// 重置所有状态
setDefaultAggregateSearch(true);
setEnableOptimization(true);
setDoubanProxyUrl('');
setEnableImageProxy(!!defaultImageProxy);
setImageProxyUrl(defaultImageProxy);
// 保存到 localStorage
if (typeof window !== 'undefined') {
localStorage.setItem('defaultAggregateSearch', JSON.stringify(true));
localStorage.setItem('enableOptimization', JSON.stringify(true));
localStorage.setItem('doubanProxyUrl', '');
localStorage.setItem(
'enableImageProxy',
JSON.stringify(!!defaultImageProxy)
);
localStorage.setItem('imageProxyUrl', defaultImageProxy);
}
};
// 设置面板内容
const settingsPanel = (
<>
@@ -95,9 +141,18 @@ export const SettingsButton: React.FC = () => {
<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] p-6'>
{/* 标题栏 */}
<div className='flex items-center justify-between mb-6'>
<h3 className='text-xl font-bold text-gray-800 dark:text-gray-200'>
</h3>
<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>
<button
onClick={handleClosePanel}
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'
@@ -176,22 +231,51 @@ export const SettingsButton: React.FC = () => {
/>
</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={enableImageProxy}
onChange={(e) => handleImageProxyToggle(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以加速图片加载
</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 ${
enableImageProxy
? '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://imageproxy.example.com/?url='
value={imageProxyUrl}
onChange={(e) => handleImageProxyUrlChange(e.target.value)}
disabled={!enableImageProxy}
/>
</div>
</div>

View File

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

View File

@@ -160,6 +160,7 @@ async function initConfig() {
SearchDownstreamMaxPage:
Number(process.env.NEXT_PUBLIC_SEARCH_MAX_PAGE) || 5,
SiteInterfaceCacheTime: fileConfig.cache_time || 7200,
ImageProxy: process.env.NEXT_PUBLIC_IMAGE_PROXY || '',
},
UserConfig: {
AllowRegister: process.env.NEXT_PUBLIC_ENABLE_REGISTER === 'true',
@@ -197,6 +198,7 @@ async function initConfig() {
SearchDownstreamMaxPage:
Number(process.env.NEXT_PUBLIC_SEARCH_MAX_PAGE) || 5,
SiteInterfaceCacheTime: fileConfig.cache_time || 7200,
ImageProxy: process.env.NEXT_PUBLIC_IMAGE_PROXY || '',
},
UserConfig: {
AllowRegister: process.env.NEXT_PUBLIC_ENABLE_REGISTER === 'true',
@@ -234,6 +236,8 @@ export async function getConfig(): Promise<AdminConfig> {
'本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。';
adminConfig.UserConfig.AllowRegister =
process.env.NEXT_PUBLIC_ENABLE_REGISTER === 'true';
adminConfig.SiteConfig.ImageProxy =
process.env.NEXT_PUBLIC_IMAGE_PROXY || '';
// 合并文件中的源信息
fileConfig = runtimeConfig as unknown as ConfigFileStruct;
@@ -302,6 +306,7 @@ export async function resetConfig() {
SearchDownstreamMaxPage:
Number(process.env.NEXT_PUBLIC_SEARCH_MAX_PAGE) || 5,
SiteInterfaceCacheTime: fileConfig.cache_time || 7200,
ImageProxy: process.env.NEXT_PUBLIC_IMAGE_PROXY || '',
},
UserConfig: {
AllowRegister: process.env.NEXT_PUBLIC_ENABLE_REGISTER === 'true',

View File

@@ -8,8 +8,22 @@ import Hls from 'hls.js';
export function getImageProxyUrl(): string | null {
if (typeof window === 'undefined') return null;
const imageProxyUrl = localStorage.getItem('imageProxyUrl');
return imageProxyUrl && imageProxyUrl.trim() ? imageProxyUrl.trim() : null;
// 本地未开启图片代理,则不使用代理
const enableImageProxy = localStorage.getItem('enableImageProxy');
if (enableImageProxy !== null) {
if (!JSON.parse(enableImageProxy) as boolean) {
return null;
} else {
// 启用,直接返回本地配置
return localStorage.getItem('imageProxyUrl')?.trim() || null;
}
}
// 如果未设置,则使用全局对象
const serverImageProxy = (window as any).RUNTIME_CONFIG?.IMAGE_PROXY;
return serverImageProxy && serverImageProxy.trim()
? serverImageProxy.trim()
: null;
}
/**
@@ -21,9 +35,6 @@ export function processImageUrl(originalUrl: string): string {
const proxyUrl = getImageProxyUrl();
if (!proxyUrl) return originalUrl;
// 如果原始 URL 已经是代理 URL则不再处理
if (originalUrl.includes(proxyUrl)) return originalUrl;
return `${proxyUrl}${encodeURIComponent(originalUrl)}`;
}