网络直播增加开关
This commit is contained in:
@@ -10734,6 +10734,9 @@ const WebLiveConfig = ({
|
|||||||
platform: 'huya',
|
platform: 'huya',
|
||||||
roomId: '',
|
roomId: '',
|
||||||
});
|
});
|
||||||
|
const [showDisclaimerModal, setShowDisclaimerModal] = useState(false);
|
||||||
|
const [countdown, setCountdown] = useState(10);
|
||||||
|
const [isEnabling, setIsEnabling] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (config?.WebLiveConfig) {
|
if (config?.WebLiveConfig) {
|
||||||
@@ -10741,6 +10744,14 @@ const WebLiveConfig = ({
|
|||||||
}
|
}
|
||||||
}, [config]);
|
}, [config]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let timer: NodeJS.Timeout;
|
||||||
|
if (showDisclaimerModal && countdown > 0) {
|
||||||
|
timer = setTimeout(() => setCountdown(countdown - 1), 1000);
|
||||||
|
}
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [showDisclaimerModal, countdown]);
|
||||||
|
|
||||||
const callApi = async (body: Record<string, any>) => {
|
const callApi = async (body: Record<string, any>) => {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch('/api/admin/web-live', {
|
const resp = await fetch('/api/admin/web-live', {
|
||||||
@@ -10798,12 +10809,111 @@ const WebLiveConfig = ({
|
|||||||
withLoading(`deleteWebLive_${key}`, () => callApi({ action: 'delete', key })).catch(() => {});
|
withLoading(`deleteWebLive_${key}`, () => callApi({ action: 'delete', key })).catch(() => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleToggleWebLiveEnabled = async () => {
|
||||||
|
const currentEnabled = config?.WebLiveEnabled ?? false;
|
||||||
|
|
||||||
|
if (!currentEnabled) {
|
||||||
|
setShowDisclaimerModal(true);
|
||||||
|
setCountdown(10);
|
||||||
|
} else {
|
||||||
|
await withLoading('toggleWebLiveEnabled', async () => {
|
||||||
|
await callApi({ action: 'toggleEnabled', enabled: false });
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmEnable = async () => {
|
||||||
|
setIsEnabling(true);
|
||||||
|
try {
|
||||||
|
await callApi({ action: 'toggleEnabled', enabled: true });
|
||||||
|
setShowDisclaimerModal(false);
|
||||||
|
setCountdown(10);
|
||||||
|
} catch (err) {
|
||||||
|
// Error already handled by callApi
|
||||||
|
} finally {
|
||||||
|
setIsEnabling(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
return <div className='text-center text-gray-500 dark:text-gray-400'>加载中...</div>;
|
return <div className='text-center text-gray-500 dark:text-gray-400'>加载中...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='space-y-6'>
|
<div className='space-y-6'>
|
||||||
|
{/* 功能总开关 */}
|
||||||
|
<div className='p-4 bg-gradient-to-r from-orange-50 to-red-50 dark:from-orange-900/20 dark:to-red-900/20 rounded-lg border-2 border-orange-300 dark:border-orange-700'>
|
||||||
|
<div className='flex items-center justify-between'>
|
||||||
|
<div className='flex-1'>
|
||||||
|
<h4 className='text-sm font-semibold text-gray-900 dark:text-gray-100 mb-1'>
|
||||||
|
网络直播功能总开关
|
||||||
|
</h4>
|
||||||
|
<p className='text-xs text-gray-600 dark:text-gray-400'>
|
||||||
|
关闭后,侧边栏和底部导航栏的网络直播入口将被隐藏,用户无法访问网络直播页面
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleToggleWebLiveEnabled}
|
||||||
|
disabled={isLoading('toggleWebLiveEnabled')}
|
||||||
|
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 ${
|
||||||
|
config.WebLiveEnabled ? buttonStyles.toggleOn : buttonStyles.toggleOff
|
||||||
|
} ${isLoading('toggleWebLiveEnabled') ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`inline-block h-4 w-4 transform rounded-full transition-transform ${
|
||||||
|
buttonStyles.toggleThumb
|
||||||
|
} ${config.WebLiveEnabled ? buttonStyles.toggleThumbOn : buttonStyles.toggleThumbOff}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 免责声明弹窗 */}
|
||||||
|
{showDisclaimerModal && createPortal(
|
||||||
|
<div className='fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4'>
|
||||||
|
<div className='bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full border border-red-200 dark:border-red-800'>
|
||||||
|
<div className='p-6'>
|
||||||
|
<div className='flex justify-center mb-4'>
|
||||||
|
<AlertTriangle className='w-12 h-12 text-red-500' />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 className='text-xl font-bold text-gray-900 dark:text-gray-100 mb-4 text-center'>
|
||||||
|
免责声明
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className='bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 mb-6'>
|
||||||
|
<p className='text-sm text-gray-700 dark:text-gray-300 leading-relaxed'>
|
||||||
|
本功能仅供个人学习和技术研究使用,请勿将其部署在公网环境中,更不得用于任何违法违规行为。
|
||||||
|
使用本功能所产生的一切法律责任由使用者自行承担,与开发者无关。
|
||||||
|
启用此功能即表示您已充分理解并同意承担相应风险。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='flex gap-3 justify-center'>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setShowDisclaimerModal(false);
|
||||||
|
setCountdown(10);
|
||||||
|
}}
|
||||||
|
className={buttonStyles.secondary}
|
||||||
|
disabled={isEnabling}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleConfirmEnable}
|
||||||
|
disabled={countdown > 0 || isEnabling}
|
||||||
|
className={countdown > 0 || isEnabling ? buttonStyles.disabled : buttonStyles.danger}
|
||||||
|
>
|
||||||
|
{isEnabling ? '启用中...' : countdown > 0 ? `确认 (${countdown}s)` : '确认启用'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body
|
||||||
|
)}
|
||||||
|
|
||||||
<div className='flex items-center justify-between'>
|
<div className='flex items-center justify-between'>
|
||||||
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300'>网络直播列表</h4>
|
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300'>网络直播列表</h4>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -27,6 +27,12 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
|
case 'toggleEnabled': {
|
||||||
|
const { enabled } = body;
|
||||||
|
config.WebLiveEnabled = enabled;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'add': {
|
case 'add': {
|
||||||
const { name, platform, roomId } = body;
|
const { name, platform, roomId } = body;
|
||||||
if (!name || !platform || !roomId) {
|
if (!name || !platform || !roomId) {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic'; // 禁用缓存
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ export default async function RootLayout({
|
|||||||
let aiDefaultMessageNoVideo = '';
|
let aiDefaultMessageNoVideo = '';
|
||||||
let aiDefaultMessageWithVideo = '';
|
let aiDefaultMessageWithVideo = '';
|
||||||
let enableMovieRequest = true;
|
let enableMovieRequest = true;
|
||||||
|
let webLiveEnabled = false;
|
||||||
let customCategories = [] as {
|
let customCategories = [] as {
|
||||||
name: string;
|
name: string;
|
||||||
type: 'movie' | 'tv';
|
type: 'movie' | 'tv';
|
||||||
@@ -127,6 +128,8 @@ export default async function RootLayout({
|
|||||||
aiDefaultMessageWithVideo = config.AIConfig?.DefaultMessageWithVideo || '';
|
aiDefaultMessageWithVideo = config.AIConfig?.DefaultMessageWithVideo || '';
|
||||||
// 求片功能配置
|
// 求片功能配置
|
||||||
enableMovieRequest = config.SiteConfig.EnableMovieRequest ?? true;
|
enableMovieRequest = config.SiteConfig.EnableMovieRequest ?? true;
|
||||||
|
// 网络直播功能配置
|
||||||
|
webLiveEnabled = config.WebLiveEnabled ?? false;
|
||||||
// 检查是否启用了 OpenList 功能
|
// 检查是否启用了 OpenList 功能
|
||||||
openListEnabled = !!(
|
openListEnabled = !!(
|
||||||
config.OpenListConfig?.Enabled &&
|
config.OpenListConfig?.Enabled &&
|
||||||
@@ -182,6 +185,7 @@ export default async function RootLayout({
|
|||||||
AI_DEFAULT_MESSAGE_NO_VIDEO: aiDefaultMessageNoVideo,
|
AI_DEFAULT_MESSAGE_NO_VIDEO: aiDefaultMessageNoVideo,
|
||||||
AI_DEFAULT_MESSAGE_WITH_VIDEO: aiDefaultMessageWithVideo,
|
AI_DEFAULT_MESSAGE_WITH_VIDEO: aiDefaultMessageWithVideo,
|
||||||
ENABLE_MOVIE_REQUEST: enableMovieRequest,
|
ENABLE_MOVIE_REQUEST: enableMovieRequest,
|
||||||
|
WEB_LIVE_ENABLED: webLiveEnabled,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import PageLayout from '@/components/PageLayout';
|
import PageLayout from '@/components/PageLayout';
|
||||||
import { Radio } from 'lucide-react';
|
import { Radio, AlertTriangle } from 'lucide-react';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
|
|
||||||
let Artplayer: any = null;
|
let Artplayer: any = null;
|
||||||
@@ -25,6 +25,7 @@ export default function WebLivePage() {
|
|||||||
const [isVideoLoading, setIsVideoLoading] = useState(false);
|
const [isVideoLoading, setIsVideoLoading] = useState(false);
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
const [selectedPlatform, setSelectedPlatform] = useState<string | null>(null);
|
const [selectedPlatform, setSelectedPlatform] = useState<string | null>(null);
|
||||||
|
const [isWebLiveEnabled, setIsWebLiveEnabled] = useState<boolean | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const meta = document.createElement('meta');
|
const meta = document.createElement('meta');
|
||||||
@@ -36,8 +37,18 @@ export default function WebLivePage() {
|
|||||||
import('artplayer').then(mod => { Artplayer = mod.default; });
|
import('artplayer').then(mod => { Artplayer = mod.default; });
|
||||||
import('hls.js').then(mod => { Hls = mod.default; });
|
import('hls.js').then(mod => { Hls = mod.default; });
|
||||||
import('flv.js').then(mod => { flvjs = mod.default; });
|
import('flv.js').then(mod => { flvjs = mod.default; });
|
||||||
|
|
||||||
|
// 检查网络直播功能是否启用
|
||||||
|
const runtimeConfig = (window as any).RUNTIME_CONFIG;
|
||||||
|
const enabled = runtimeConfig?.WEB_LIVE_ENABLED ?? false;
|
||||||
|
setIsWebLiveEnabled(enabled);
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
|
fetchSources();
|
||||||
|
} else {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fetchSources();
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.head.removeChild(meta);
|
document.head.removeChild(meta);
|
||||||
@@ -239,6 +250,33 @@ export default function WebLivePage() {
|
|||||||
setSelectedPlatform(null);
|
setSelectedPlatform(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 如果功能未启用,显示提示
|
||||||
|
if (isWebLiveEnabled === false) {
|
||||||
|
return (
|
||||||
|
<PageLayout activePath='/web-live'>
|
||||||
|
<div className='flex items-center justify-center min-h-screen bg-transparent'>
|
||||||
|
<div className='text-center max-w-md mx-auto px-6'>
|
||||||
|
<div className='relative mb-8'>
|
||||||
|
<div className='relative mx-auto w-24 h-24 bg-gradient-to-r from-orange-500 to-red-600 rounded-2xl shadow-2xl flex items-center justify-center transform hover:scale-105 transition-transform duration-300'>
|
||||||
|
<AlertTriangle className='w-12 h-12 text-white' />
|
||||||
|
<div className='absolute -inset-2 bg-gradient-to-r from-orange-500 to-red-600 rounded-2xl opacity-20 animate-pulse'></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='space-y-4'>
|
||||||
|
<h3 className='text-2xl font-bold text-gray-900 dark:text-gray-100'>功能未启用</h3>
|
||||||
|
<div className='bg-orange-50 dark:bg-orange-900/20 border border-orange-200 dark:border-orange-800 rounded-lg p-4'>
|
||||||
|
<p className='text-sm text-gray-700 dark:text-gray-300 leading-relaxed'>
|
||||||
|
网络直播功能当前未启用。请联系管理员在管理面板中开启此功能。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PageLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<PageLayout activePath='/web-live'>
|
<PageLayout activePath='/web-live'>
|
||||||
|
|||||||
@@ -93,12 +93,16 @@ const MobileBottomNav = ({ activePath }: MobileBottomNavProps) => {
|
|||||||
label: '电视直播',
|
label: '电视直播',
|
||||||
href: '/live',
|
href: '/live',
|
||||||
},
|
},
|
||||||
{
|
];
|
||||||
|
|
||||||
|
// 如果启用网络直播,添加网络直播入口
|
||||||
|
if (runtimeConfig?.WEB_LIVE_ENABLED) {
|
||||||
|
items.push({
|
||||||
icon: Globe,
|
icon: Globe,
|
||||||
label: '网络直播',
|
label: '网络直播',
|
||||||
href: '/web-live',
|
href: '/web-live',
|
||||||
},
|
});
|
||||||
];
|
}
|
||||||
|
|
||||||
// 如果配置了 OpenList 或 Emby,添加私人影库入口
|
// 如果配置了 OpenList 或 Emby,添加私人影库入口
|
||||||
if (runtimeConfig?.PRIVATE_LIBRARY_ENABLED) {
|
if (runtimeConfig?.PRIVATE_LIBRARY_ENABLED) {
|
||||||
|
|||||||
@@ -184,12 +184,16 @@ const Sidebar = ({ onToggle, activePath = '/' }: SidebarProps) => {
|
|||||||
label: '电视直播',
|
label: '电视直播',
|
||||||
href: '/live',
|
href: '/live',
|
||||||
},
|
},
|
||||||
{
|
];
|
||||||
|
|
||||||
|
// 如果启用网络直播,添加网络直播入口
|
||||||
|
if (runtimeConfig?.WEB_LIVE_ENABLED) {
|
||||||
|
items.push({
|
||||||
icon: Globe,
|
icon: Globe,
|
||||||
label: '网络直播',
|
label: '网络直播',
|
||||||
href: '/web-live',
|
href: '/web-live',
|
||||||
},
|
});
|
||||||
];
|
}
|
||||||
|
|
||||||
// 如果配置了 OpenList 或 Emby,添加私人影库入口
|
// 如果配置了 OpenList 或 Emby,添加私人影库入口
|
||||||
if (runtimeConfig?.PRIVATE_LIBRARY_ENABLED) {
|
if (runtimeConfig?.PRIVATE_LIBRARY_ENABLED) {
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ export interface AdminConfig {
|
|||||||
from: 'config' | 'custom';
|
from: 'config' | 'custom';
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}[];
|
}[];
|
||||||
|
WebLiveEnabled?: boolean; // 网络直播功能总开关
|
||||||
ThemeConfig?: {
|
ThemeConfig?: {
|
||||||
enableBuiltInTheme: boolean; // 是否启用内置主题
|
enableBuiltInTheme: boolean; // 是否启用内置主题
|
||||||
builtInTheme: string; // 内置主题名称
|
builtInTheme: string; // 内置主题名称
|
||||||
|
|||||||
Reference in New Issue
Block a user