From 214aee1cb439c9391dd377d3a5c0e33c345b2710 Mon Sep 17 00:00:00 2001 From: shinya Date: Wed, 13 Aug 2025 13:53:31 +0800 Subject: [PATCH] feat: config subscription --- src/app/admin/page.tsx | 132 +++++++++++++++++- src/app/api/admin/config_file/route.ts | 13 +- .../admin/config_subscription/fetch/route.ts | 36 +++++ 3 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 src/app/api/admin/config_subscription/fetch/route.ts diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index abfb261..640068a 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -1317,15 +1317,61 @@ const CategoryConfig = ({ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig | null; refreshConfig: () => Promise }) => { const [configContent, setConfigContent] = useState(''); const [saving, setSaving] = useState(false); + const [subscriptionUrl, setSubscriptionUrl] = useState(''); + const [autoUpdate, setAutoUpdate] = useState(false); + const [fetching, setFetching] = useState(false); + const [lastCheckTime, setLastCheckTime] = useState(''); useEffect(() => { if (config?.ConfigFile) { setConfigContent(config.ConfigFile); } + if (config?.ConfigSubscribtion) { + setSubscriptionUrl(config.ConfigSubscribtion.URL); + setAutoUpdate(config.ConfigSubscribtion.AutoUpdate); + setLastCheckTime(config.ConfigSubscribtion.LastCheck || ''); + } }, [config]); + // 拉取订阅配置 + const handleFetchConfig = async () => { + if (!subscriptionUrl.trim()) { + showError('请输入订阅URL'); + return; + } + + try { + setFetching(true); + const resp = await fetch('/api/admin/config_subscription/fetch', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url: subscriptionUrl }), + }); + + if (!resp.ok) { + const data = await resp.json().catch(() => ({})); + throw new Error(data.error || `拉取失败: ${resp.status}`); + } + + const data = await resp.json(); + if (data.configContent) { + setConfigContent(data.configContent); + // 更新本地配置的最后检查时间 + const currentTime = new Date().toISOString(); + setLastCheckTime(currentTime); + showSuccess('配置拉取成功'); + } else { + showError('拉取失败:未获取到配置内容'); + } + } catch (err) { + showError(err instanceof Error ? err.message : '拉取失败'); + } finally { + setFetching(false); + } + }; + // 保存配置文件 const handleSave = async () => { try { @@ -1333,7 +1379,12 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig | const resp = await fetch('/api/admin/config_file', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ configFile: configContent }), + body: JSON.stringify({ + configFile: configContent, + subscriptionUrl, + autoUpdate, + lastCheckTime: lastCheckTime || new Date().toISOString() + }), }); if (!resp.ok) { @@ -1362,6 +1413,85 @@ const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig | return (
+ {/* 配置订阅区域 */} +
+
+

+ 配置订阅 +

+
+ 最后更新: {lastCheckTime ? new Date(lastCheckTime).toLocaleString('zh-CN') : '从未更新'} +
+
+ +
+ {/* 订阅URL输入 */} +
+ + setSubscriptionUrl(e.target.value)} + placeholder='https://example.com/config.json' + className='w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-green-500 focus:border-transparent transition-all duration-200 shadow-sm hover:border-gray-400 dark:hover:border-gray-500' + /> +

+ 输入配置文件的订阅地址,支持JSON格式 +

+
+ + {/* 自动更新开关 */} +
+
+ +

+ 启用后系统将定期自动拉取最新配置 +

+
+ +
+ + {/* 拉取配置按钮 */} +
+ +
+
+
+ {/* 配置文件编辑区域 */}
diff --git a/src/app/api/admin/config_file/route.ts b/src/app/api/admin/config_file/route.ts index ec44a25..48e7513 100644 --- a/src/app/api/admin/config_file/route.ts +++ b/src/app/api/admin/config_file/route.ts @@ -40,7 +40,7 @@ export async function POST(request: NextRequest) { // 获取请求体 const body = await request.json(); - const { configFile } = body; + const { configFile, subscriptionUrl, autoUpdate, lastCheckTime } = body; if (!configFile || typeof configFile !== 'string') { return NextResponse.json( @@ -60,6 +60,17 @@ export async function POST(request: NextRequest) { } adminConfig.ConfigFile = configFile; + + // 更新订阅配置 + if (subscriptionUrl !== undefined) { + adminConfig.ConfigSubscribtion.URL = subscriptionUrl; + } + if (autoUpdate !== undefined) { + adminConfig.ConfigSubscribtion.AutoUpdate = autoUpdate; + } + // 更新最后检查时间 - 使用前端传递的时间或当前时间 + adminConfig.ConfigSubscribtion.LastCheck = lastCheckTime || ''; + adminConfig = refineConfig(adminConfig); // 更新配置文件 if (storage && typeof (storage as any).setAdminConfig === 'function') { diff --git a/src/app/api/admin/config_subscription/fetch/route.ts b/src/app/api/admin/config_subscription/fetch/route.ts new file mode 100644 index 0000000..4cde5f6 --- /dev/null +++ b/src/app/api/admin/config_subscription/fetch/route.ts @@ -0,0 +1,36 @@ +import { NextRequest, NextResponse } from 'next/server'; + +export async function POST(request: NextRequest) { + try { + const { url } = await request.json(); + + if (!url) { + return NextResponse.json({ error: '缺少URL参数' }, { status: 400 }); + } + + // 直接 fetch URL 获取配置内容 + const response = await fetch(url); + + if (!response.ok) { + return NextResponse.json( + { error: `请求失败: ${response.status} ${response.statusText}` }, + { status: response.status } + ); + } + + const configContent = await response.text(); + + return NextResponse.json({ + success: true, + configContent, + message: '配置拉取成功' + }); + + } catch (error) { + console.error('拉取配置失败:', error); + return NextResponse.json( + { error: '拉取配置失败' }, + { status: 500 } + ); + } +}