feat: init 2.0.0 config file edit

This commit is contained in:
shinya
2025-08-13 00:30:31 +08:00
parent b4ed660eca
commit b6d9810ee8
14 changed files with 671 additions and 3060 deletions

View File

@@ -55,7 +55,6 @@ COPY --from=builder --chown=nextjs:nodejs /app/start.js ./start.js
# 从构建器中复制 public 和 .next/static 目录
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nodejs /app/config.json ./config.json
# 切换到非特权用户
USER nextjs

View File

@@ -3,8 +3,8 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "pnpm gen:runtime && pnpm gen:manifest && next dev -H 0.0.0.0",
"build": "pnpm gen:runtime && pnpm gen:manifest && next build",
"dev": "pnpm gen:manifest && next dev -H 0.0.0.0",
"build": "pnpm gen:manifest && next build",
"start": "next start",
"lint": "next lint",
"lint:fix": "eslint src --fix && pnpm format",
@@ -14,13 +14,11 @@
"test": "jest",
"format": "prettier -w .",
"format:check": "prettier -c .",
"gen:runtime": "node scripts/convert-config.js",
"gen:manifest": "node scripts/generate-manifest.js",
"postbuild": "echo 'Build completed - sitemap generation disabled'",
"prepare": "husky install"
},
"dependencies": {
"@cloudflare/next-on-pages": "^1.13.12",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",

2526
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,61 +0,0 @@
#!/usr/bin/env node
/* eslint-disable */
// AUTO-GENERATED SCRIPT: Converts config.json to TypeScript definition.
// Usage: node scripts/convert-config.js
const fs = require('fs');
const path = require('path');
// Resolve project root (one level up from scripts folder)
const projectRoot = path.resolve(__dirname, '..');
// Paths
const configPath = path.join(projectRoot, 'config.json');
const libDir = path.join(projectRoot, 'src', 'lib');
const oldRuntimePath = path.join(libDir, 'runtime.ts');
const newRuntimePath = path.join(libDir, 'runtime.ts');
// Delete the old runtime.ts file if it exists
if (fs.existsSync(oldRuntimePath)) {
fs.unlinkSync(oldRuntimePath);
console.log('旧的 runtime.ts 已删除');
}
// Read and parse config.json
let rawConfig;
try {
rawConfig = fs.readFileSync(configPath, 'utf8');
} catch (err) {
console.error(`无法读取 ${configPath}:`, err);
process.exit(1);
}
let config;
try {
config = JSON.parse(rawConfig);
} catch (err) {
console.error('config.json 不是有效的 JSON:', err);
process.exit(1);
}
// Prepare TypeScript file content
const tsContent =
`// 该文件由 scripts/convert-config.js 自动生成,请勿手动修改\n` +
`/* eslint-disable */\n\n` +
`export const config = ${JSON.stringify(config, null, 2)} as const;\n\n` +
`export type RuntimeConfig = typeof config;\n\n` +
`export default config;\n`;
// Ensure lib directory exists
if (!fs.existsSync(libDir)) {
fs.mkdirSync(libDir, { recursive: true });
}
// Write to runtime.ts
try {
fs.writeFileSync(newRuntimePath, tsContent, 'utf8');
console.log('已生成 src/lib/runtime.ts');
} catch (err) {
console.error('写入 runtime.ts 失败:', err);
process.exit(1);
}

View File

@@ -26,6 +26,7 @@ import {
ChevronDown,
ChevronUp,
ExternalLink,
FileText,
FolderOpen,
Settings,
Users,
@@ -313,8 +314,7 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
</h4>
<div className='flex items-center justify-between'>
<label
className={`text-gray-700 dark:text-gray-300 ${
isUpstashStorage ? 'opacity-50' : ''
className={`text-gray-700 dark:text-gray-300 ${isUpstashStorage ? 'opacity-50' : ''
}`}
>
@@ -330,15 +330,13 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
toggleAllowRegister(!userSettings.enableRegistration)
}
disabled={isUpstashStorage}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 ${
userSettings.enableRegistration
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 ${userSettings.enableRegistration
? 'bg-green-600'
: 'bg-gray-200 dark:bg-gray-700'
} ${isUpstashStorage ? 'opacity-50 cursor-not-allowed' : ''}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
userSettings.enableRegistration
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${userSettings.enableRegistration
? 'translate-x-6'
: 'translate-x-1'
}`}
@@ -521,8 +519,7 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
</td>
<td className='px-6 py-4 whitespace-nowrap'>
<span
className={`px-2 py-1 text-xs rounded-full ${
user.role === 'owner'
className={`px-2 py-1 text-xs rounded-full ${user.role === 'owner'
? 'bg-yellow-100 dark:bg-yellow-900/20 text-yellow-800 dark:text-yellow-300'
: user.role === 'admin'
? 'bg-purple-100 dark:bg-purple-900/20 text-purple-800 dark:text-purple-300'
@@ -538,8 +535,7 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
</td>
<td className='px-6 py-4 whitespace-nowrap'>
<span
className={`px-2 py-1 text-xs rounded-full ${
!user.banned
className={`px-2 py-1 text-xs rounded-full ${!user.banned
? 'bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-300'
: 'bg-red-100 dark:bg-red-900/20 text-red-800 dark:text-red-300'
}`}
@@ -793,8 +789,7 @@ const VideoSourceConfig = ({
</td>
<td className='px-6 py-4 whitespace-nowrap max-w-[1rem]'>
<span
className={`px-2 py-1 text-xs rounded-full ${
!source.disabled
className={`px-2 py-1 text-xs rounded-full ${!source.disabled
? 'bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-300'
: 'bg-red-100 dark:bg-red-900/20 text-red-800 dark:text-red-300'
}`}
@@ -805,8 +800,7 @@ const VideoSourceConfig = ({
<td className='px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2'>
<button
onClick={() => handleToggleEnable(source.key)}
className={`inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium ${
!source.disabled
className={`inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium ${!source.disabled
? 'bg-red-100 dark:bg-red-900/40 text-red-800 dark:text-red-300 hover:bg-red-200 dark:hover:bg-red-900/60'
: 'bg-green-100 dark:bg-green-900/40 text-green-800 dark:text-green-300 hover:bg-green-200 dark:hover:bg-green-900/60'
} transition-colors`}
@@ -1113,8 +1107,7 @@ const CategoryConfig = ({
className='hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors select-none'
>
<td
className={`px-2 py-4 ${
isUpstashStorage ? 'text-gray-200' : 'cursor-grab text-gray-400'
className={`px-2 py-4 ${isUpstashStorage ? 'text-gray-200' : 'cursor-grab text-gray-400'
}`}
style={{ touchAction: 'none' }}
{...(isUpstashStorage ? {} : { ...attributes, ...listeners })}
@@ -1126,8 +1119,7 @@ const CategoryConfig = ({
</td>
<td className='px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100'>
<span
className={`px-2 py-1 text-xs rounded-full ${
category.type === 'movie'
className={`px-2 py-1 text-xs rounded-full ${category.type === 'movie'
? 'bg-blue-100 dark:bg-blue-900/20 text-blue-800 dark:text-blue-300'
: 'bg-purple-100 dark:bg-purple-900/20 text-purple-800 dark:text-purple-300'
}`}
@@ -1143,8 +1135,7 @@ const CategoryConfig = ({
</td>
<td className='px-6 py-4 whitespace-nowrap max-w-[1rem]'>
<span
className={`px-2 py-1 text-xs rounded-full ${
!category.disabled
className={`px-2 py-1 text-xs rounded-full ${!category.disabled
? 'bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-300'
: 'bg-red-100 dark:bg-red-900/20 text-red-800 dark:text-red-300'
}`}
@@ -1159,8 +1150,7 @@ const CategoryConfig = ({
handleToggleEnable(category.query, category.type)
}
disabled={isUpstashStorage}
className={`inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium ${
isUpstashStorage
className={`inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium ${isUpstashStorage
? 'bg-gray-400 cursor-not-allowed text-white'
: !category.disabled
? 'bg-red-100 dark:bg-red-900/40 text-red-800 dark:text-red-300 hover:bg-red-200 dark:hover:bg-red-900/60'
@@ -1205,8 +1195,7 @@ const CategoryConfig = ({
<button
onClick={() => !isUpstashStorage && setShowAddForm(!showAddForm)}
disabled={isUpstashStorage}
className={`px-3 py-1 text-sm rounded-lg transition-colors ${
isUpstashStorage
className={`px-3 py-1 text-sm rounded-lg transition-colors ${isUpstashStorage
? 'bg-gray-400 cursor-not-allowed text-white'
: 'bg-green-600 hover:bg-green-700 text-white'
}`}
@@ -1324,6 +1313,92 @@ const CategoryConfig = ({
);
};
// 新增配置文件组件
const ConfigFileComponent = ({ config, refreshConfig }: { config: AdminConfig | null; refreshConfig: () => Promise<void> }) => {
const [configContent, setConfigContent] = useState('');
const [saving, setSaving] = useState(false);
useEffect(() => {
if (config?.ConfigFile) {
setConfigContent(config.ConfigFile);
}
}, [config]);
// 保存配置文件
const handleSave = async () => {
try {
setSaving(true);
const resp = await fetch('/api/admin/config_file', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ configFile: configContent }),
});
if (!resp.ok) {
const data = await resp.json().catch(() => ({}));
throw new Error(data.error || `保存失败: ${resp.status}`);
}
showSuccess('配置文件保存成功');
await refreshConfig();
} catch (err) {
showError(err instanceof Error ? err.message : '保存失败');
} finally {
setSaving(false);
}
};
if (!config) {
return (
<div className='text-center text-gray-500 dark:text-gray-400'>
...
</div>
);
}
return (
<div className='space-y-4'>
{/* 配置文件编辑区域 */}
<div className='space-y-4'>
<div className='relative'>
<textarea
value={configContent}
onChange={(e) => setConfigContent(e.target.value)}
rows={20}
placeholder='请输入配置文件内容JSON 格式)...'
className='w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 font-mono text-sm leading-relaxed resize-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 hover:border-gray-400 dark:hover:border-gray-500'
style={{
fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace'
}}
spellCheck={false}
data-gramm={false}
/>
</div>
<div className='flex items-center justify-between'>
<div className='text-xs text-gray-500 dark:text-gray-400'>
JSON
</div>
<button
onClick={handleSave}
disabled={saving}
className={`px-4 py-2 rounded-lg transition-colors ${saving
? 'bg-gray-400 cursor-not-allowed text-white'
: 'bg-blue-600 hover:bg-blue-700 text-white'
}`}
>
{saving ? '保存中…' : '保存配置文件'}
</button>
</div>
</div>
</div>
);
};
// 新增站点配置组件
const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
const [siteSettings, setSiteSettings] = useState<SiteConfig>({
@@ -1500,8 +1575,7 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
{/* 站点名称 */}
<div>
<label
className={`block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 ${
isUpstashStorage ? 'opacity-50' : ''
className={`block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 ${isUpstashStorage ? 'opacity-50' : ''
}`}
>
@@ -1519,8 +1593,7 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
setSiteSettings((prev) => ({ ...prev, SiteName: e.target.value }))
}
disabled={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 ${
isUpstashStorage ? 'opacity-50 cursor-not-allowed' : ''
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 ${isUpstashStorage ? 'opacity-50 cursor-not-allowed' : ''
}`}
/>
</div>
@@ -1528,8 +1601,7 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
{/* 站点公告 */}
<div>
<label
className={`block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 ${
isUpstashStorage ? 'opacity-50' : ''
className={`block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 ${isUpstashStorage ? 'opacity-50' : ''
}`}
>
@@ -1550,8 +1622,7 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
}
disabled={isUpstashStorage}
rows={3}
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 ${
isUpstashStorage ? 'opacity-50 cursor-not-allowed' : ''
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 ${isUpstashStorage ? 'opacity-50 cursor-not-allowed' : ''
}`}
/>
</div>
@@ -1560,8 +1631,7 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
<div className='space-y-3'>
<div>
<label
className={`block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 ${
isUpstashStorage ? 'opacity-50' : ''
className={`block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 ${isUpstashStorage ? 'opacity-50' : ''
}`}
>
@@ -1577,8 +1647,7 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
type='button'
onClick={() => setIsDoubanDropdownOpen(!isDoubanDropdownOpen)}
disabled={isUpstashStorage}
className={`w-full px-3 py-2.5 pr-10 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-all duration-200 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-sm hover:border-gray-400 dark:hover:border-gray-500 text-left ${
isUpstashStorage ? 'opacity-50 cursor-not-allowed' : ''
className={`w-full px-3 py-2.5 pr-10 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-all duration-200 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-sm hover:border-gray-400 dark:hover:border-gray-500 text-left ${isUpstashStorage ? 'opacity-50 cursor-not-allowed' : ''
}`}
>
{
@@ -1591,8 +1660,7 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
{/* 下拉箭头 */}
<div className='absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none'>
<ChevronDown
className={`w-4 h-4 text-gray-400 dark:text-gray-500 transition-transform duration-200 ${
isDoubanDropdownOpen ? 'rotate-180' : ''
className={`w-4 h-4 text-gray-400 dark:text-gray-500 transition-transform duration-200 ${isDoubanDropdownOpen ? 'rotate-180' : ''
}`}
/>
</div>
@@ -1608,8 +1676,7 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
handleDoubanDataSourceChange(option.value);
setIsDoubanDropdownOpen(false);
}}
className={`w-full px-3 py-2.5 text-left text-sm transition-colors duration-150 flex items-center justify-between hover:bg-gray-100 dark:hover:bg-gray-700 ${
siteSettings.DoubanProxyType === option.value
className={`w-full px-3 py-2.5 text-left text-sm transition-colors duration-150 flex items-center justify-between hover:bg-gray-100 dark:hover:bg-gray-700 ${siteSettings.DoubanProxyType === option.value
? 'bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400'
: 'text-gray-900 dark:text-gray-100'
}`}
@@ -1653,8 +1720,7 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
{siteSettings.DoubanProxyType === 'custom' && (
<div>
<label
className={`block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 ${
isUpstashStorage ? 'opacity-50' : ''
className={`block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 ${isUpstashStorage ? 'opacity-50' : ''
}`}
>
@@ -1671,8 +1737,7 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
}))
}
disabled={isUpstashStorage}
className={`w-full px-3 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-all duration-200 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 shadow-sm hover:border-gray-400 dark:hover:border-gray-500 ${
isUpstashStorage ? 'opacity-50 cursor-not-allowed' : ''
className={`w-full px-3 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-all duration-200 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 shadow-sm hover:border-gray-400 dark:hover:border-gray-500 ${isUpstashStorage ? 'opacity-50 cursor-not-allowed' : ''
}`}
/>
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
@@ -1686,8 +1751,7 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
<div className='space-y-3'>
<div>
<label
className={`block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 ${
isUpstashStorage ? 'opacity-50' : ''
className={`block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 ${isUpstashStorage ? 'opacity-50' : ''
}`}
>
@@ -1707,8 +1771,7 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
)
}
disabled={isUpstashStorage}
className={`w-full px-3 py-2.5 pr-10 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-all duration-200 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-sm hover:border-gray-400 dark:hover:border-gray-500 text-left ${
isUpstashStorage ? 'opacity-50 cursor-not-allowed' : ''
className={`w-full px-3 py-2.5 pr-10 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-all duration-200 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-sm hover:border-gray-400 dark:hover:border-gray-500 text-left ${isUpstashStorage ? 'opacity-50 cursor-not-allowed' : ''
}`}
>
{
@@ -1721,8 +1784,7 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
{/* 下拉箭头 */}
<div className='absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none'>
<ChevronDown
className={`w-4 h-4 text-gray-400 dark:text-gray-500 transition-transform duration-200 ${
isDoubanImageProxyDropdownOpen ? 'rotate-180' : ''
className={`w-4 h-4 text-gray-400 dark:text-gray-500 transition-transform duration-200 ${isDoubanImageProxyDropdownOpen ? 'rotate-180' : ''
}`}
/>
</div>
@@ -1738,8 +1800,7 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
handleDoubanImageProxyChange(option.value);
setIsDoubanImageProxyDropdownOpen(false);
}}
className={`w-full px-3 py-2.5 text-left text-sm transition-colors duration-150 flex items-center justify-between hover:bg-gray-100 dark:hover:bg-gray-700 ${
siteSettings.DoubanImageProxyType === option.value
className={`w-full px-3 py-2.5 text-left text-sm transition-colors duration-150 flex items-center justify-between hover:bg-gray-100 dark:hover:bg-gray-700 ${siteSettings.DoubanImageProxyType === option.value
? 'bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400'
: 'text-gray-900 dark:text-gray-100'
}`}
@@ -1783,8 +1844,7 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
{siteSettings.DoubanImageProxyType === 'custom' && (
<div>
<label
className={`block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 ${
isUpstashStorage ? 'opacity-50' : ''
className={`block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 ${isUpstashStorage ? 'opacity-50' : ''
}`}
>
@@ -1801,8 +1861,7 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
}))
}
disabled={isUpstashStorage}
className={`w-full px-3 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-all duration-200 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 shadow-sm hover:border-gray-400 dark:hover:border-gray-500 ${
isUpstashStorage ? 'opacity-50 cursor-not-allowed' : ''
className={`w-full px-3 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 transition-all duration-200 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 dark:placeholder-gray-400 shadow-sm hover:border-gray-400 dark:hover:border-gray-500 ${isUpstashStorage ? 'opacity-50 cursor-not-allowed' : ''
}`}
/>
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
@@ -1854,8 +1913,7 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
<div>
<div className='flex items-center justify-between'>
<label
className={`block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 ${
isUpstashStorage ? 'opacity-50' : ''
className={`block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 ${isUpstashStorage ? 'opacity-50' : ''
}`}
>
@@ -1875,15 +1933,13 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
}))
}
disabled={isUpstashStorage}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 ${
siteSettings.DisableYellowFilter
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 ${siteSettings.DisableYellowFilter
? 'bg-green-600'
: 'bg-gray-200 dark:bg-gray-700'
} ${isUpstashStorage ? 'opacity-50 cursor-not-allowed' : ''}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
siteSettings.DisableYellowFilter
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${siteSettings.DisableYellowFilter
? 'translate-x-6'
: 'translate-x-1'
}`}
@@ -1900,8 +1956,7 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
<button
onClick={handleSave}
disabled={saving}
className={`px-4 py-2 ${
saving
className={`px-4 py-2 ${saving
? 'bg-gray-400 cursor-not-allowed'
: 'bg-green-600 hover:bg-green-700'
} text-white rounded-lg transition-colors`}
@@ -1923,6 +1978,7 @@ function AdminPageClient() {
videoSource: false,
siteConfig: false,
categoryConfig: false,
configFile: false,
});
// 获取管理员配置
@@ -2036,6 +2092,21 @@ function AdminPageClient() {
)}
</div>
{/* 配置文件标签 */}
<CollapsibleTab
title='配置文件'
icon={
<FileText
size={20}
className='text-gray-600 dark:text-gray-400'
/>
}
isExpanded={expandedTabs.configFile}
onToggle={() => toggleTab('configFile')}
>
<ConfigFileComponent config={config} refreshConfig={fetchConfig} />
</CollapsibleTab>
{/* 站点配置标签 */}
<CollapsibleTab
title='站点配置'

View File

@@ -0,0 +1,88 @@
/* eslint-disable no-console,@typescript-eslint/no-explicit-any */
import { NextRequest, NextResponse } from 'next/server';
import { getAuthInfoFromCookie } from '@/lib/auth';
import { getConfig, refineConfig } from '@/lib/config';
import { getStorage } from '@/lib/db';
export async function POST(request: NextRequest) {
const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage';
if (storageType === 'localstorage') {
return NextResponse.json(
{
error: '不支持本地存储进行管理员配置',
},
{ status: 400 }
);
}
const authInfo = getAuthInfoFromCookie(request);
if (!authInfo || !authInfo.username) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const username = authInfo.username;
try {
// 检查用户权限
let adminConfig = await getConfig();
const storage = getStorage();
if (username !== process.env.USERNAME) {
const user = adminConfig.UserConfig.Users.find((u) => u.username === username);
if (!user || user.role !== 'admin' || user.banned) {
return NextResponse.json(
{ error: '权限不足,只有管理员可以修改配置文件' },
{ status: 403 }
);
}
}
// 获取请求体
const body = await request.json();
const { configFile } = body;
if (!configFile || typeof configFile !== 'string') {
return NextResponse.json(
{ error: '配置文件内容不能为空' },
{ status: 400 }
);
}
// 验证 JSON 格式
try {
JSON.parse(configFile);
} catch (e) {
return NextResponse.json(
{ error: '配置文件格式错误,请检查 JSON 语法' },
{ status: 400 }
);
}
adminConfig.ConfigFile = configFile;
adminConfig = refineConfig(adminConfig);
// 更新配置文件
if (storage && typeof (storage as any).setAdminConfig === 'function') {
await (storage as any).setAdminConfig(adminConfig);
return NextResponse.json({
success: true,
message: '配置文件更新成功',
});
} else {
return NextResponse.json(
{ error: '存储服务不可用' },
{ status: 500 }
);
}
} catch (error) {
console.error('更新配置文件失败:', error);
return NextResponse.json(
{
error: '更新配置文件失败',
details: (error as Error).message,
},
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,13 @@
/* eslint-disable no-console */
import { NextRequest, NextResponse } from "next/server";
import { getConfig } from "@/lib/config";
export const runtime = 'edge';
export async function GET(req: NextRequest) {
console.log('custom_category', req.url);
const config = await getConfig();
return NextResponse.json(config.CustomCategories);
}

View File

@@ -7,6 +7,7 @@ import { Suspense } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { GetBangumiCalendarData } from '@/lib/bangumi.client';
import { getCustomCategories } from '@/lib/config.client';
import {
getDoubanCategories,
getDoubanList,
@@ -80,10 +81,9 @@ function DoubanPageClient() {
// 获取自定义分类数据
useEffect(() => {
const runtimeConfig = (window as any).RUNTIME_CONFIG;
if (runtimeConfig?.CUSTOM_CATEGORIES?.length > 0) {
setCustomCategories(runtimeConfig.CUSTOM_CATEGORIES);
}
getCustomCategories().then((categories) => {
setCustomCategories(categories);
});
}, []);
// 同步最新参数值到 ref

View File

@@ -7,7 +7,6 @@ import './globals.css';
import 'sweetalert2/dist/sweetalert2.min.css';
import { getConfig } from '@/lib/config';
import RuntimeConfig from '@/lib/runtime';
import { GlobalErrorIndicator } from '../components/GlobalErrorIndicator';
import { SiteProvider } from '../components/SiteProvider';
@@ -39,6 +38,8 @@ export default async function RootLayout({
}: {
children: React.ReactNode;
}) {
const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage';
let siteName = process.env.NEXT_PUBLIC_SITE_NAME || 'MoonTV';
let announcement =
process.env.ANNOUNCEMENT ||
@@ -51,13 +52,7 @@ export default async function RootLayout({
let doubanImageProxy = process.env.NEXT_PUBLIC_DOUBAN_IMAGE_PROXY || '';
let disableYellowFilter =
process.env.NEXT_PUBLIC_DISABLE_YELLOW_FILTER === 'true';
let customCategories =
(RuntimeConfig as any).custom_category?.map((category: any) => ({
name: 'name' in category ? category.name : '',
type: category.type,
query: category.query,
})) || ([] as Array<{ name: string; type: 'movie' | 'tv'; query: string }>);
if (process.env.NEXT_PUBLIC_STORAGE_TYPE !== 'upstash') {
if (storageType !== 'upstash' && storageType !== 'localstorage') {
const config = await getConfig();
siteName = config.SiteConfig.SiteName;
announcement = config.SiteConfig.Announcement;
@@ -67,13 +62,6 @@ export default async function RootLayout({
doubanImageProxyType = config.SiteConfig.DoubanImageProxyType;
doubanImageProxy = config.SiteConfig.DoubanImageProxy;
disableYellowFilter = config.SiteConfig.DisableYellowFilter;
customCategories = config.CustomCategories.filter(
(category) => !category.disabled
).map((category) => ({
name: category.name || '',
type: category.type,
query: category.query,
}));
}
// 将运行时配置注入到全局 window 对象,供客户端在运行时读取
@@ -85,7 +73,6 @@ export default async function RootLayout({
DOUBAN_IMAGE_PROXY_TYPE: doubanImageProxyType,
DOUBAN_IMAGE_PROXY: doubanImageProxy,
DISABLE_YELLOW_FILTER: disableYellowFilter,
CUSTOM_CATEGORIES: customCategories,
};
return (

View File

@@ -7,6 +7,8 @@ import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useEffect, useState } from 'react';
import { getCustomCategories } from '@/lib/config.client';
interface MobileBottomNavProps {
/**
* 主动指定当前激活的路径。当未提供时,自动使用 usePathname() 获取的路径。
@@ -46,8 +48,8 @@ const MobileBottomNav = ({ activePath }: MobileBottomNavProps) => {
]);
useEffect(() => {
const runtimeConfig = (window as any).RUNTIME_CONFIG;
if (runtimeConfig?.CUSTOM_CATEGORIES?.length > 0) {
getCustomCategories().then((categories) => {
if (categories.length > 0) {
setNavItems((prevItems) => [
...prevItems,
{
@@ -57,6 +59,7 @@ const MobileBottomNav = ({ activePath }: MobileBottomNavProps) => {
},
]);
}
});
}, []);
const isActive = (href: string) => {
@@ -97,8 +100,7 @@ const MobileBottomNav = ({ activePath }: MobileBottomNavProps) => {
className='flex flex-col items-center justify-center w-full h-14 gap-1 text-xs'
>
<item.icon
className={`h-6 w-6 ${
active
className={`h-6 w-6 ${active
? 'text-green-600 dark:text-green-400'
: 'text-gray-500 dark:text-gray-400'
}`}

View File

@@ -14,6 +14,8 @@ import {
useState,
} from 'react';
import { getCustomCategories } from '@/lib/config.client';
import { useSite } from './SiteProvider';
interface SidebarContextType {
@@ -148,8 +150,8 @@ const Sidebar = ({ onToggle, activePath = '/' }: SidebarProps) => {
]);
useEffect(() => {
const runtimeConfig = (window as any).RUNTIME_CONFIG;
if (runtimeConfig?.CUSTOM_CATEGORIES?.length > 0) {
getCustomCategories().then((categories) => {
if (categories.length > 0) {
setMenuItems((prevItems) => [
...prevItems,
{
@@ -159,6 +161,7 @@ const Sidebar = ({ onToggle, activePath = '/' }: SidebarProps) => {
},
]);
}
});
}, []);
return (
@@ -167,8 +170,7 @@ const Sidebar = ({ onToggle, activePath = '/' }: SidebarProps) => {
<div className='hidden md:flex'>
<aside
data-sidebar
className={`fixed top-0 left-0 h-screen bg-white/40 backdrop-blur-xl transition-all duration-300 border-r border-gray-200/50 z-10 shadow-lg dark:bg-gray-900/70 dark:border-gray-700/50 ${
isCollapsed ? 'w-16' : 'w-64'
className={`fixed top-0 left-0 h-screen bg-white/40 backdrop-blur-xl transition-all duration-300 border-r border-gray-200/50 z-10 shadow-lg dark:bg-gray-900/70 dark:border-gray-700/50 ${isCollapsed ? 'w-16' : 'w-64'
}`}
style={{
backdropFilter: 'blur(20px)',
@@ -179,8 +181,7 @@ const Sidebar = ({ onToggle, activePath = '/' }: SidebarProps) => {
{/* 顶部 Logo 区域 */}
<div className='relative h-16'>
<div
className={`absolute inset-0 flex items-center justify-center transition-opacity duration-200 ${
isCollapsed ? 'opacity-0' : 'opacity-100'
className={`absolute inset-0 flex items-center justify-center transition-opacity duration-200 ${isCollapsed ? 'opacity-0' : 'opacity-100'
}`}
>
<div className='w-[calc(100%-4rem)] flex justify-center'>
@@ -189,8 +190,7 @@ const Sidebar = ({ onToggle, activePath = '/' }: SidebarProps) => {
</div>
<button
onClick={handleToggle}
className={`absolute top-1/2 -translate-y-1/2 flex items-center justify-center w-8 h-8 rounded-lg text-gray-500 hover:text-gray-700 hover:bg-gray-100/50 transition-colors duration-200 z-10 dark:text-gray-400 dark:hover:text-gray-200 dark:hover:bg-gray-700/50 ${
isCollapsed ? 'left-1/2 -translate-x-1/2' : 'right-2'
className={`absolute top-1/2 -translate-y-1/2 flex items-center justify-center w-8 h-8 rounded-lg text-gray-500 hover:text-gray-700 hover:bg-gray-100/50 transition-colors duration-200 z-10 dark:text-gray-400 dark:hover:text-gray-200 dark:hover:bg-gray-700/50 ${isCollapsed ? 'left-1/2 -translate-x-1/2' : 'right-2'
}`}
>
<Menu className='h-4 w-4' />
@@ -203,8 +203,7 @@ const Sidebar = ({ onToggle, activePath = '/' }: SidebarProps) => {
href='/'
onClick={() => setActive('/')}
data-active={active === '/'}
className={`group flex items-center rounded-lg px-2 py-2 pl-4 text-gray-700 hover:bg-gray-100/30 hover:text-green-600 data-[active=true]:bg-green-500/20 data-[active=true]:text-green-700 font-medium transition-colors duration-200 min-h-[40px] dark:text-gray-300 dark:hover:text-green-400 dark:data-[active=true]:bg-green-500/10 dark:data-[active=true]:text-green-400 ${
isCollapsed ? 'w-full max-w-none mx-0' : 'mx-0'
className={`group flex items-center rounded-lg px-2 py-2 pl-4 text-gray-700 hover:bg-gray-100/30 hover:text-green-600 data-[active=true]:bg-green-500/20 data-[active=true]:text-green-700 font-medium transition-colors duration-200 min-h-[40px] dark:text-gray-300 dark:hover:text-green-400 dark:data-[active=true]:bg-green-500/10 dark:data-[active=true]:text-green-400 ${isCollapsed ? 'w-full max-w-none mx-0' : 'mx-0'
} gap-3 justify-start`}
>
<div className='w-4 h-4 flex items-center justify-center'>
@@ -224,8 +223,7 @@ const Sidebar = ({ onToggle, activePath = '/' }: SidebarProps) => {
setActive('/search');
}}
data-active={active === '/search'}
className={`group flex items-center rounded-lg px-2 py-2 pl-4 text-gray-700 hover:bg-gray-100/30 hover:text-green-600 data-[active=true]:bg-green-500/20 data-[active=true]:text-green-700 font-medium transition-colors duration-200 min-h-[40px] dark:text-gray-300 dark:hover:text-green-400 dark:data-[active=true]:bg-green-500/10 dark:data-[active=true]:text-green-400 ${
isCollapsed ? 'w-full max-w-none mx-0' : 'mx-0'
className={`group flex items-center rounded-lg px-2 py-2 pl-4 text-gray-700 hover:bg-gray-100/30 hover:text-green-600 data-[active=true]:bg-green-500/20 data-[active=true]:text-green-700 font-medium transition-colors duration-200 min-h-[40px] dark:text-gray-300 dark:hover:text-green-400 dark:data-[active=true]:bg-green-500/10 dark:data-[active=true]:text-green-400 ${isCollapsed ? 'w-full max-w-none mx-0' : 'mx-0'
} gap-3 justify-start`}
>
<div className='w-4 h-4 flex items-center justify-center'>
@@ -261,8 +259,7 @@ const Sidebar = ({ onToggle, activePath = '/' }: SidebarProps) => {
href={item.href}
onClick={() => setActive(item.href)}
data-active={isActive}
className={`group flex items-center rounded-lg px-2 py-2 pl-4 text-sm text-gray-700 hover:bg-gray-100/30 hover:text-green-600 data-[active=true]:bg-green-500/20 data-[active=true]:text-green-700 transition-colors duration-200 min-h-[40px] dark:text-gray-300 dark:hover:text-green-400 dark:data-[active=true]:bg-green-500/10 dark:data-[active=true]:text-green-400 ${
isCollapsed ? 'w-full max-w-none mx-0' : 'mx-0'
className={`group flex items-center rounded-lg px-2 py-2 pl-4 text-sm text-gray-700 hover:bg-gray-100/30 hover:text-green-600 data-[active=true]:bg-green-500/20 data-[active=true]:text-green-700 transition-colors duration-200 min-h-[40px] dark:text-gray-300 dark:hover:text-green-400 dark:data-[active=true]:bg-green-500/10 dark:data-[active=true]:text-green-400 ${isCollapsed ? 'w-full max-w-none mx-0' : 'mx-0'
} gap-3 justify-start`}
>
<div className='w-4 h-4 flex items-center justify-center'>
@@ -281,8 +278,7 @@ const Sidebar = ({ onToggle, activePath = '/' }: SidebarProps) => {
</div>
</aside>
<div
className={`transition-all duration-300 sidebar-offset ${
isCollapsed ? 'w-16' : 'w-64'
className={`transition-all duration-300 sidebar-offset ${isCollapsed ? 'w-16' : 'w-64'
}`}
></div>
</div>

View File

@@ -1,4 +1,5 @@
export interface AdminConfig {
ConfigFile: string;
SiteConfig: {
SiteName: string;
Announcement: string;

17
src/lib/config.client.ts Normal file
View File

@@ -0,0 +1,17 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client';
export async function getCustomCategories(): Promise<{
name: string;
type: 'movie' | 'tv';
query: string;
}[]> {
const res = await fetch('/api/config/custom_category');
const data = await res.json();
return data.filter((item: any) => !item.disabled).map((category: any) => ({
name: category.name || '',
type: category.type,
query: category.query,
}));
}

View File

@@ -3,7 +3,6 @@
import { getStorage } from '@/lib/db';
import { AdminConfig } from './admin.types';
import runtimeConfig from './runtime';
export interface ApiSite {
key: string;
@@ -14,7 +13,7 @@ export interface ApiSite {
interface ConfigFileStruct {
cache_time?: number;
api_site: {
api_site?: {
[key: string]: ApiSite;
};
custom_category?: {
@@ -48,27 +47,98 @@ export const API_CONFIG = {
let fileConfig: ConfigFileStruct;
let cachedConfig: AdminConfig;
export function refineConfig(adminConfig: AdminConfig): AdminConfig {
try {
fileConfig = JSON.parse(adminConfig.ConfigFile) as ConfigFileStruct;
} catch (e) {
fileConfig = {} as ConfigFileStruct;
}
// 合并文件中的源信息
const apiSiteEntries = Object.entries(fileConfig.api_site || []);
const sourceConfigMap = new Map(
(adminConfig.SourceConfig || []).map((s) => [s.key, s])
);
apiSiteEntries.forEach(([key, site]) => {
const existingSource = sourceConfigMap.get(key);
if (existingSource) {
// 如果已存在,只覆盖 name、api、detail 和 from
existingSource.name = site.name;
existingSource.api = site.api;
existingSource.detail = site.detail;
existingSource.from = 'config';
} else {
// 如果不存在,创建新条目
sourceConfigMap.set(key, {
key,
name: site.name,
api: site.api,
detail: site.detail,
from: 'config',
disabled: false,
});
}
});
// 检查现有源是否在 fileConfig.api_site 中,如果不在则标记为 custom
const apiSiteKeys = new Set(apiSiteEntries.map(([key]) => key));
sourceConfigMap.forEach((source) => {
if (!apiSiteKeys.has(source.key)) {
source.from = 'custom';
}
});
// 将 Map 转换回数组
adminConfig.SourceConfig = Array.from(sourceConfigMap.values());
// 覆盖 CustomCategories
const customCategories = fileConfig.custom_category || [];
const customCategoriesMap = new Map(
(adminConfig.CustomCategories || []).map((c) => [c.query + c.type, c])
);
customCategories.forEach((category) => {
const key = category.query + category.type;
const existedCategory = customCategoriesMap.get(key);
if (existedCategory) {
existedCategory.name = category.name;
existedCategory.query = category.query;
existedCategory.type = category.type;
existedCategory.from = 'config';
} else {
customCategoriesMap.set(key, {
name: category.name,
type: category.type,
query: category.query,
from: 'config',
disabled: false,
});
}
});
// 检查现有 CustomCategories 是否在 fileConfig.custom_category 中,如果不在则标记为 custom
const customCategoriesKeys = new Set(
customCategories.map((c) => c.query + c.type)
);
customCategoriesMap.forEach((category) => {
if (!customCategoriesKeys.has(category.query + category.type)) {
category.from = 'custom';
}
});
// 将 Map 转换回数组
adminConfig.CustomCategories = Array.from(customCategoriesMap.values());
return adminConfig;
}
async function initConfig() {
if (cachedConfig) {
// 自检补全配置
cachedConfig = refineConfig(cachedConfig);
return;
}
if (process.env.DOCKER_ENV === 'true') {
// eslint-disable-next-line @typescript-eslint/no-implied-eval
const _require = eval('require') as NodeRequire;
const fs = _require('fs') as typeof import('fs');
const path = _require('path') as typeof import('path');
const configPath = path.join(process.cwd(), 'config.json');
const raw = fs.readFileSync(configPath, 'utf-8');
fileConfig = JSON.parse(raw) as ConfigFileStruct;
console.log('load dynamic config success');
} else {
// 默认使用编译时生成的配置
fileConfig = runtimeConfig as unknown as ConfigFileStruct;
}
const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage';
if (storageType !== 'localstorage') {
// 数据库存储,读取并补全管理员配置
const storage = getStorage();
@@ -89,11 +159,16 @@ async function initConfig() {
}
}
// 从文件中获取源信息,用于补全源
const apiSiteEntries = Object.entries(fileConfig.api_site);
if (adminConfig) {
try {
fileConfig = JSON.parse(adminConfig.ConfigFile) as ConfigFileStruct;
} catch (e) {
console.error('解析配置文件失败:', e);
fileConfig = {} as ConfigFileStruct;
}
const apiSiteEntries = Object.entries(fileConfig.api_site || []);
const customCategories = fileConfig.custom_category || [];
if (adminConfig) {
// 补全 SourceConfig
const sourceConfigMap = new Map(
(adminConfig.SourceConfig || []).map((s) => [s.key, s])
@@ -177,6 +252,7 @@ async function initConfig() {
});
}
} else {
fileConfig = {} as ConfigFileStruct;
// 数据库中没有配置,创建新的管理员配置
let allUsers = userNames.map((uname) => ({
username: uname,
@@ -191,6 +267,7 @@ async function initConfig() {
});
}
adminConfig = {
ConfigFile: '',
SiteConfig: {
SiteName: process.env.NEXT_PUBLIC_SITE_NAME || 'MoonTV',
Announcement:
@@ -212,21 +289,8 @@ async function initConfig() {
AllowRegister: process.env.NEXT_PUBLIC_ENABLE_REGISTER === 'true',
Users: allUsers as any,
},
SourceConfig: apiSiteEntries.map(([key, site]) => ({
key,
name: site.name,
api: site.api,
detail: site.detail,
from: 'config',
disabled: false,
})),
CustomCategories: customCategories.map((category) => ({
name: category.name,
type: category.type,
query: category.query,
from: 'config',
disabled: false,
})),
SourceConfig: [],
CustomCategories: [],
};
}
@@ -240,52 +304,10 @@ async function initConfig() {
} catch (err) {
console.error('加载管理员配置失败:', err);
}
} else {
// 本地存储直接使用文件配置
cachedConfig = {
SiteConfig: {
SiteName: process.env.NEXT_PUBLIC_SITE_NAME || 'MoonTV',
Announcement:
process.env.ANNOUNCEMENT ||
'本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。',
SearchDownstreamMaxPage:
Number(process.env.NEXT_PUBLIC_SEARCH_MAX_PAGE) || 5,
SiteInterfaceCacheTime: fileConfig.cache_time || 7200,
DoubanProxyType: process.env.NEXT_PUBLIC_DOUBAN_PROXY_TYPE || 'direct',
DoubanProxy: process.env.NEXT_PUBLIC_DOUBAN_PROXY || '',
DoubanImageProxyType:
process.env.NEXT_PUBLIC_DOUBAN_IMAGE_PROXY_TYPE || 'direct',
DoubanImageProxy: process.env.NEXT_PUBLIC_DOUBAN_IMAGE_PROXY || '',
DisableYellowFilter:
process.env.NEXT_PUBLIC_DISABLE_YELLOW_FILTER === 'true',
},
UserConfig: {
AllowRegister: process.env.NEXT_PUBLIC_ENABLE_REGISTER === 'true',
Users: [],
},
SourceConfig: Object.entries(fileConfig.api_site).map(([key, site]) => ({
key,
name: site.name,
api: site.api,
detail: site.detail,
from: 'config',
disabled: false,
})),
CustomCategories:
fileConfig.custom_category?.map((category) => ({
name: category.name,
type: category.type,
query: category.query,
from: 'config',
disabled: false,
})) || [],
} as AdminConfig;
}
}
export async function getConfig(): Promise<AdminConfig> {
const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage';
if (process.env.DOCKER_ENV === 'true' || storageType === 'localstorage') {
if (process.env.DOCKER_ENV === 'true') {
await initConfig();
return cachedConfig;
}
@@ -320,9 +342,15 @@ export async function getConfig(): Promise<AdminConfig> {
adminConfig.SiteConfig.DisableYellowFilter =
process.env.NEXT_PUBLIC_DISABLE_YELLOW_FILTER === 'true';
try {
fileConfig = JSON.parse(adminConfig.ConfigFile) as ConfigFileStruct;
} catch (e) {
console.error('解析配置文件失败:', e);
fileConfig = {} as ConfigFileStruct;
}
// 合并文件中的源信息
fileConfig = runtimeConfig as unknown as ConfigFileStruct;
const apiSiteEntries = Object.entries(fileConfig.api_site);
const apiSiteEntries = Object.entries(fileConfig.api_site || []);
const sourceConfigMap = new Map(
(adminConfig.SourceConfig || []).map((s) => [s.key, s])
);
@@ -398,8 +426,19 @@ export async function getConfig(): Promise<AdminConfig> {
}
export async function resetConfig() {
const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage';
let originConfig: AdminConfig | null = null;
const storage = getStorage();
if (storage && typeof (storage as any).getAdminConfig === 'function') {
originConfig = await (storage as any).getAdminConfig();
}
if (originConfig) {
fileConfig = JSON.parse(originConfig.ConfigFile) as ConfigFileStruct;
} else {
fileConfig = {} as ConfigFileStruct;
}
const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE;
// 获取所有用户名,用于补全 Users
let userNames: string[] = [];
if (storage && typeof (storage as any).getAllUsers === 'function') {
@@ -410,22 +449,7 @@ export async function resetConfig() {
}
}
if (process.env.DOCKER_ENV === 'true') {
// eslint-disable-next-line @typescript-eslint/no-implied-eval
const _require = eval('require') as NodeRequire;
const fs = _require('fs') as typeof import('fs');
const path = _require('path') as typeof import('path');
const configPath = path.join(process.cwd(), 'config.json');
const raw = fs.readFileSync(configPath, 'utf-8');
fileConfig = JSON.parse(raw) as ConfigFileStruct;
console.log('load dynamic config success');
} else {
// 默认使用编译时生成的配置
fileConfig = runtimeConfig as unknown as ConfigFileStruct;
}
const apiSiteEntries = Object.entries(fileConfig.api_site);
const apiSiteEntries = Object.entries(fileConfig.api_site || []);
const customCategories = fileConfig.custom_category || [];
let allUsers = userNames.map((uname) => ({
username: uname,
@@ -440,6 +464,7 @@ export async function resetConfig() {
});
}
const adminConfig = {
ConfigFile: originConfig?.ConfigFile || '',
SiteConfig: {
SiteName: process.env.NEXT_PUBLIC_SITE_NAME || 'MoonTV',
Announcement:
@@ -487,10 +512,11 @@ export async function resetConfig() {
// serverless 环境,直接使用 adminConfig
cachedConfig = adminConfig;
}
cachedConfig.ConfigFile = adminConfig.ConfigFile;
cachedConfig.SiteConfig = adminConfig.SiteConfig;
cachedConfig.UserConfig = adminConfig.UserConfig;
cachedConfig.SourceConfig = adminConfig.SourceConfig;
cachedConfig.CustomCategories = adminConfig.CustomCategories;
cachedConfig.CustomCategories = adminConfig.CustomCategories || [];
}
export async function getCacheTime(): Promise<number> {