mirror of
https://github.com/MoonTechLab/LunaTV.git
synced 2026-02-21 00:44:41 +08:00
feat: add global image proxy config
This commit is contained in:
@@ -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 | (空) |
|
||||
|
||||
## 配置说明
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
// 写入数据库
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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()}
|
||||
>
|
||||
{/* 封面 */}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -4,6 +4,7 @@ export interface AdminConfig {
|
||||
Announcement: string;
|
||||
SearchDownstreamMaxPage: number;
|
||||
SiteInterfaceCacheTime: number;
|
||||
ImageProxy: string;
|
||||
};
|
||||
UserConfig: {
|
||||
AllowRegister: boolean;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)}`;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user