feat: init 2.0.0 config file edit
This commit is contained in:
@@ -55,7 +55,6 @@ COPY --from=builder --chown=nextjs:nodejs /app/start.js ./start.js
|
|||||||
# 从构建器中复制 public 和 .next/static 目录
|
# 从构建器中复制 public 和 .next/static 目录
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
|
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/.next/static ./.next/static
|
||||||
COPY --from=builder --chown=nextjs:nodejs /app/config.json ./config.json
|
|
||||||
|
|
||||||
# 切换到非特权用户
|
# 切换到非特权用户
|
||||||
USER nextjs
|
USER nextjs
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "pnpm gen:runtime && pnpm gen:manifest && next dev -H 0.0.0.0",
|
"dev": "pnpm gen:manifest && next dev -H 0.0.0.0",
|
||||||
"build": "pnpm gen:runtime && pnpm gen:manifest && next build",
|
"build": "pnpm gen:manifest && next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"lint:fix": "eslint src --fix && pnpm format",
|
"lint:fix": "eslint src --fix && pnpm format",
|
||||||
@@ -14,13 +14,11 @@
|
|||||||
"test": "jest",
|
"test": "jest",
|
||||||
"format": "prettier -w .",
|
"format": "prettier -w .",
|
||||||
"format:check": "prettier -c .",
|
"format:check": "prettier -c .",
|
||||||
"gen:runtime": "node scripts/convert-config.js",
|
|
||||||
"gen:manifest": "node scripts/generate-manifest.js",
|
"gen:manifest": "node scripts/generate-manifest.js",
|
||||||
"postbuild": "echo 'Build completed - sitemap generation disabled'",
|
"postbuild": "echo 'Build completed - sitemap generation disabled'",
|
||||||
"prepare": "husky install"
|
"prepare": "husky install"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/next-on-pages": "^1.13.12",
|
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/modifiers": "^9.0.0",
|
"@dnd-kit/modifiers": "^9.0.0",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
@@ -89,4 +87,4 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.14.0"
|
"packageManager": "pnpm@10.14.0"
|
||||||
}
|
}
|
||||||
2526
pnpm-lock.yaml
generated
2526
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
|
FileText,
|
||||||
FolderOpen,
|
FolderOpen,
|
||||||
Settings,
|
Settings,
|
||||||
Users,
|
Users,
|
||||||
@@ -313,9 +314,8 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
|
|||||||
</h4>
|
</h4>
|
||||||
<div className='flex items-center justify-between'>
|
<div className='flex items-center justify-between'>
|
||||||
<label
|
<label
|
||||||
className={`text-gray-700 dark:text-gray-300 ${
|
className={`text-gray-700 dark:text-gray-300 ${isUpstashStorage ? 'opacity-50' : ''
|
||||||
isUpstashStorage ? 'opacity-50' : ''
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
允许新用户注册
|
允许新用户注册
|
||||||
{isUpstashStorage && (
|
{isUpstashStorage && (
|
||||||
@@ -330,18 +330,16 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
|
|||||||
toggleAllowRegister(!userSettings.enableRegistration)
|
toggleAllowRegister(!userSettings.enableRegistration)
|
||||||
}
|
}
|
||||||
disabled={isUpstashStorage}
|
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 ${
|
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
|
||||||
userSettings.enableRegistration
|
? 'bg-green-600'
|
||||||
? 'bg-green-600'
|
: 'bg-gray-200 dark:bg-gray-700'
|
||||||
: 'bg-gray-200 dark:bg-gray-700'
|
} ${isUpstashStorage ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||||
} ${isUpstashStorage ? 'opacity-50 cursor-not-allowed' : ''}`}
|
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${userSettings.enableRegistration
|
||||||
userSettings.enableRegistration
|
? 'translate-x-6'
|
||||||
? 'translate-x-6'
|
: 'translate-x-1'
|
||||||
: 'translate-x-1'
|
}`}
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -521,28 +519,26 @@ const UserConfig = ({ config, role, refreshConfig }: UserConfigProps) => {
|
|||||||
</td>
|
</td>
|
||||||
<td className='px-6 py-4 whitespace-nowrap'>
|
<td className='px-6 py-4 whitespace-nowrap'>
|
||||||
<span
|
<span
|
||||||
className={`px-2 py-1 text-xs rounded-full ${
|
className={`px-2 py-1 text-xs rounded-full ${user.role === 'owner'
|
||||||
user.role === 'owner'
|
? 'bg-yellow-100 dark:bg-yellow-900/20 text-yellow-800 dark:text-yellow-300'
|
||||||
? 'bg-yellow-100 dark:bg-yellow-900/20 text-yellow-800 dark:text-yellow-300'
|
: user.role === 'admin'
|
||||||
: user.role === 'admin'
|
|
||||||
? 'bg-purple-100 dark:bg-purple-900/20 text-purple-800 dark:text-purple-300'
|
? 'bg-purple-100 dark:bg-purple-900/20 text-purple-800 dark:text-purple-300'
|
||||||
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
|
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{user.role === 'owner'
|
{user.role === 'owner'
|
||||||
? '站长'
|
? '站长'
|
||||||
: user.role === 'admin'
|
: user.role === 'admin'
|
||||||
? '管理员'
|
? '管理员'
|
||||||
: '普通用户'}
|
: '普通用户'}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className='px-6 py-4 whitespace-nowrap'>
|
<td className='px-6 py-4 whitespace-nowrap'>
|
||||||
<span
|
<span
|
||||||
className={`px-2 py-1 text-xs rounded-full ${
|
className={`px-2 py-1 text-xs rounded-full ${!user.banned
|
||||||
!user.banned
|
? 'bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-300'
|
||||||
? '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'
|
||||||
: 'bg-red-100 dark:bg-red-900/20 text-red-800 dark:text-red-300'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{!user.banned ? '正常' : '已封禁'}
|
{!user.banned ? '正常' : '已封禁'}
|
||||||
</span>
|
</span>
|
||||||
@@ -793,11 +789,10 @@ const VideoSourceConfig = ({
|
|||||||
</td>
|
</td>
|
||||||
<td className='px-6 py-4 whitespace-nowrap max-w-[1rem]'>
|
<td className='px-6 py-4 whitespace-nowrap max-w-[1rem]'>
|
||||||
<span
|
<span
|
||||||
className={`px-2 py-1 text-xs rounded-full ${
|
className={`px-2 py-1 text-xs rounded-full ${!source.disabled
|
||||||
!source.disabled
|
? 'bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-300'
|
||||||
? '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'
|
||||||
: 'bg-red-100 dark:bg-red-900/20 text-red-800 dark:text-red-300'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{!source.disabled ? '启用中' : '已禁用'}
|
{!source.disabled ? '启用中' : '已禁用'}
|
||||||
</span>
|
</span>
|
||||||
@@ -805,11 +800,10 @@ const VideoSourceConfig = ({
|
|||||||
<td className='px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2'>
|
<td className='px-6 py-4 whitespace-nowrap text-right text-sm font-medium space-x-2'>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleToggleEnable(source.key)}
|
onClick={() => handleToggleEnable(source.key)}
|
||||||
className={`inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium ${
|
className={`inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium ${!source.disabled
|
||||||
!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-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'
|
||||||
: '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`}
|
||||||
} transition-colors`}
|
|
||||||
>
|
>
|
||||||
{!source.disabled ? '禁用' : '启用'}
|
{!source.disabled ? '禁用' : '启用'}
|
||||||
</button>
|
</button>
|
||||||
@@ -1113,9 +1107,8 @@ const CategoryConfig = ({
|
|||||||
className='hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors select-none'
|
className='hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors select-none'
|
||||||
>
|
>
|
||||||
<td
|
<td
|
||||||
className={`px-2 py-4 ${
|
className={`px-2 py-4 ${isUpstashStorage ? 'text-gray-200' : 'cursor-grab text-gray-400'
|
||||||
isUpstashStorage ? 'text-gray-200' : 'cursor-grab text-gray-400'
|
}`}
|
||||||
}`}
|
|
||||||
style={{ touchAction: 'none' }}
|
style={{ touchAction: 'none' }}
|
||||||
{...(isUpstashStorage ? {} : { ...attributes, ...listeners })}
|
{...(isUpstashStorage ? {} : { ...attributes, ...listeners })}
|
||||||
>
|
>
|
||||||
@@ -1126,11 +1119,10 @@ const CategoryConfig = ({
|
|||||||
</td>
|
</td>
|
||||||
<td className='px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100'>
|
<td className='px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100'>
|
||||||
<span
|
<span
|
||||||
className={`px-2 py-1 text-xs rounded-full ${
|
className={`px-2 py-1 text-xs rounded-full ${category.type === 'movie'
|
||||||
category.type === 'movie'
|
? 'bg-blue-100 dark:bg-blue-900/20 text-blue-800 dark:text-blue-300'
|
||||||
? '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'
|
||||||
: 'bg-purple-100 dark:bg-purple-900/20 text-purple-800 dark:text-purple-300'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{category.type === 'movie' ? '电影' : '电视剧'}
|
{category.type === 'movie' ? '电影' : '电视剧'}
|
||||||
</span>
|
</span>
|
||||||
@@ -1143,11 +1135,10 @@ const CategoryConfig = ({
|
|||||||
</td>
|
</td>
|
||||||
<td className='px-6 py-4 whitespace-nowrap max-w-[1rem]'>
|
<td className='px-6 py-4 whitespace-nowrap max-w-[1rem]'>
|
||||||
<span
|
<span
|
||||||
className={`px-2 py-1 text-xs rounded-full ${
|
className={`px-2 py-1 text-xs rounded-full ${!category.disabled
|
||||||
!category.disabled
|
? 'bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-300'
|
||||||
? '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'
|
||||||
: 'bg-red-100 dark:bg-red-900/20 text-red-800 dark:text-red-300'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{!category.disabled ? '启用中' : '已禁用'}
|
{!category.disabled ? '启用中' : '已禁用'}
|
||||||
</span>
|
</span>
|
||||||
@@ -1159,13 +1150,12 @@ const CategoryConfig = ({
|
|||||||
handleToggleEnable(category.query, category.type)
|
handleToggleEnable(category.query, category.type)
|
||||||
}
|
}
|
||||||
disabled={isUpstashStorage}
|
disabled={isUpstashStorage}
|
||||||
className={`inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium ${
|
className={`inline-flex items-center px-3 py-1.5 rounded-full text-xs font-medium ${isUpstashStorage
|
||||||
isUpstashStorage
|
? 'bg-gray-400 cursor-not-allowed text-white'
|
||||||
? 'bg-gray-400 cursor-not-allowed text-white'
|
: !category.disabled
|
||||||
: !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'
|
? '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'
|
: '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`}
|
} transition-colors`}
|
||||||
>
|
>
|
||||||
{!category.disabled ? '禁用' : '启用'}
|
{!category.disabled ? '禁用' : '启用'}
|
||||||
</button>
|
</button>
|
||||||
@@ -1205,11 +1195,10 @@ const CategoryConfig = ({
|
|||||||
<button
|
<button
|
||||||
onClick={() => !isUpstashStorage && setShowAddForm(!showAddForm)}
|
onClick={() => !isUpstashStorage && setShowAddForm(!showAddForm)}
|
||||||
disabled={isUpstashStorage}
|
disabled={isUpstashStorage}
|
||||||
className={`px-3 py-1 text-sm rounded-lg transition-colors ${
|
className={`px-3 py-1 text-sm rounded-lg transition-colors ${isUpstashStorage
|
||||||
isUpstashStorage
|
? 'bg-gray-400 cursor-not-allowed text-white'
|
||||||
? 'bg-gray-400 cursor-not-allowed text-white'
|
: 'bg-green-600 hover:bg-green-700 text-white'
|
||||||
: 'bg-green-600 hover:bg-green-700 text-white'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{showAddForm ? '取消' : '添加分类'}
|
{showAddForm ? '取消' : '添加分类'}
|
||||||
</button>
|
</button>
|
||||||
@@ -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 SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
|
||||||
const [siteSettings, setSiteSettings] = useState<SiteConfig>({
|
const [siteSettings, setSiteSettings] = useState<SiteConfig>({
|
||||||
@@ -1500,9 +1575,8 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
|
|||||||
{/* 站点名称 */}
|
{/* 站点名称 */}
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
className={`block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 ${
|
className={`block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 ${isUpstashStorage ? 'opacity-50' : ''
|
||||||
isUpstashStorage ? 'opacity-50' : ''
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
站点名称
|
站点名称
|
||||||
{isUpstashStorage && (
|
{isUpstashStorage && (
|
||||||
@@ -1519,18 +1593,16 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
|
|||||||
setSiteSettings((prev) => ({ ...prev, SiteName: e.target.value }))
|
setSiteSettings((prev) => ({ ...prev, SiteName: e.target.value }))
|
||||||
}
|
}
|
||||||
disabled={isUpstashStorage}
|
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 ${
|
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' : ''
|
||||||
isUpstashStorage ? 'opacity-50 cursor-not-allowed' : ''
|
}`}
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 站点公告 */}
|
{/* 站点公告 */}
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
className={`block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 ${
|
className={`block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 ${isUpstashStorage ? 'opacity-50' : ''
|
||||||
isUpstashStorage ? 'opacity-50' : ''
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
站点公告
|
站点公告
|
||||||
{isUpstashStorage && (
|
{isUpstashStorage && (
|
||||||
@@ -1550,9 +1622,8 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
|
|||||||
}
|
}
|
||||||
disabled={isUpstashStorage}
|
disabled={isUpstashStorage}
|
||||||
rows={3}
|
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 ${
|
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' : ''
|
||||||
isUpstashStorage ? 'opacity-50 cursor-not-allowed' : ''
|
}`}
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1560,9 +1631,8 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
|
|||||||
<div className='space-y-3'>
|
<div className='space-y-3'>
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
className={`block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 ${
|
className={`block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 ${isUpstashStorage ? 'opacity-50' : ''
|
||||||
isUpstashStorage ? 'opacity-50' : ''
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
豆瓣数据代理
|
豆瓣数据代理
|
||||||
{isUpstashStorage && (
|
{isUpstashStorage && (
|
||||||
@@ -1577,9 +1647,8 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
|
|||||||
type='button'
|
type='button'
|
||||||
onClick={() => setIsDoubanDropdownOpen(!isDoubanDropdownOpen)}
|
onClick={() => setIsDoubanDropdownOpen(!isDoubanDropdownOpen)}
|
||||||
disabled={isUpstashStorage}
|
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 ${
|
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' : ''
|
||||||
isUpstashStorage ? 'opacity-50 cursor-not-allowed' : ''
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
doubanDataSourceOptions.find(
|
doubanDataSourceOptions.find(
|
||||||
@@ -1591,9 +1660,8 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
|
|||||||
{/* 下拉箭头 */}
|
{/* 下拉箭头 */}
|
||||||
<div className='absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none'>
|
<div className='absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none'>
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
className={`w-4 h-4 text-gray-400 dark:text-gray-500 transition-transform duration-200 ${
|
className={`w-4 h-4 text-gray-400 dark:text-gray-500 transition-transform duration-200 ${isDoubanDropdownOpen ? 'rotate-180' : ''
|
||||||
isDoubanDropdownOpen ? 'rotate-180' : ''
|
}`}
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1608,11 +1676,10 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
|
|||||||
handleDoubanDataSourceChange(option.value);
|
handleDoubanDataSourceChange(option.value);
|
||||||
setIsDoubanDropdownOpen(false);
|
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 ${
|
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
|
||||||
siteSettings.DoubanProxyType === option.value
|
? 'bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400'
|
||||||
? 'bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400'
|
: 'text-gray-900 dark:text-gray-100'
|
||||||
: 'text-gray-900 dark:text-gray-100'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<span className='truncate'>{option.label}</span>
|
<span className='truncate'>{option.label}</span>
|
||||||
{siteSettings.DoubanProxyType === option.value && (
|
{siteSettings.DoubanProxyType === option.value && (
|
||||||
@@ -1653,9 +1720,8 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
|
|||||||
{siteSettings.DoubanProxyType === 'custom' && (
|
{siteSettings.DoubanProxyType === 'custom' && (
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
className={`block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 ${
|
className={`block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 ${isUpstashStorage ? 'opacity-50' : ''
|
||||||
isUpstashStorage ? 'opacity-50' : ''
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
豆瓣代理地址
|
豆瓣代理地址
|
||||||
</label>
|
</label>
|
||||||
@@ -1671,9 +1737,8 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
disabled={isUpstashStorage}
|
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 ${
|
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' : ''
|
||||||
isUpstashStorage ? 'opacity-50 cursor-not-allowed' : ''
|
}`}
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
|
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
|
||||||
自定义代理服务器地址
|
自定义代理服务器地址
|
||||||
@@ -1686,9 +1751,8 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
|
|||||||
<div className='space-y-3'>
|
<div className='space-y-3'>
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
className={`block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 ${
|
className={`block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 ${isUpstashStorage ? 'opacity-50' : ''
|
||||||
isUpstashStorage ? 'opacity-50' : ''
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
豆瓣图片代理
|
豆瓣图片代理
|
||||||
{isUpstashStorage && (
|
{isUpstashStorage && (
|
||||||
@@ -1707,9 +1771,8 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
disabled={isUpstashStorage}
|
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 ${
|
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' : ''
|
||||||
isUpstashStorage ? 'opacity-50 cursor-not-allowed' : ''
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
doubanImageProxyTypeOptions.find(
|
doubanImageProxyTypeOptions.find(
|
||||||
@@ -1721,9 +1784,8 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
|
|||||||
{/* 下拉箭头 */}
|
{/* 下拉箭头 */}
|
||||||
<div className='absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none'>
|
<div className='absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none'>
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
className={`w-4 h-4 text-gray-400 dark:text-gray-500 transition-transform duration-200 ${
|
className={`w-4 h-4 text-gray-400 dark:text-gray-500 transition-transform duration-200 ${isDoubanImageProxyDropdownOpen ? 'rotate-180' : ''
|
||||||
isDoubanImageProxyDropdownOpen ? 'rotate-180' : ''
|
}`}
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1738,11 +1800,10 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
|
|||||||
handleDoubanImageProxyChange(option.value);
|
handleDoubanImageProxyChange(option.value);
|
||||||
setIsDoubanImageProxyDropdownOpen(false);
|
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 ${
|
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
|
||||||
siteSettings.DoubanImageProxyType === option.value
|
? 'bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400'
|
||||||
? 'bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400'
|
: 'text-gray-900 dark:text-gray-100'
|
||||||
: 'text-gray-900 dark:text-gray-100'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<span className='truncate'>{option.label}</span>
|
<span className='truncate'>{option.label}</span>
|
||||||
{siteSettings.DoubanImageProxyType === option.value && (
|
{siteSettings.DoubanImageProxyType === option.value && (
|
||||||
@@ -1783,9 +1844,8 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
|
|||||||
{siteSettings.DoubanImageProxyType === 'custom' && (
|
{siteSettings.DoubanImageProxyType === 'custom' && (
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
className={`block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 ${
|
className={`block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 ${isUpstashStorage ? 'opacity-50' : ''
|
||||||
isUpstashStorage ? 'opacity-50' : ''
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
豆瓣图片代理地址
|
豆瓣图片代理地址
|
||||||
</label>
|
</label>
|
||||||
@@ -1801,9 +1861,8 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
disabled={isUpstashStorage}
|
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 ${
|
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' : ''
|
||||||
isUpstashStorage ? 'opacity-50 cursor-not-allowed' : ''
|
}`}
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
|
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
|
||||||
自定义图片代理服务器地址
|
自定义图片代理服务器地址
|
||||||
@@ -1854,9 +1913,8 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
|
|||||||
<div>
|
<div>
|
||||||
<div className='flex items-center justify-between'>
|
<div className='flex items-center justify-between'>
|
||||||
<label
|
<label
|
||||||
className={`block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 ${
|
className={`block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 ${isUpstashStorage ? 'opacity-50' : ''
|
||||||
isUpstashStorage ? 'opacity-50' : ''
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
禁用黄色过滤器
|
禁用黄色过滤器
|
||||||
{isUpstashStorage && (
|
{isUpstashStorage && (
|
||||||
@@ -1875,18 +1933,16 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
disabled={isUpstashStorage}
|
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 ${
|
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
|
||||||
siteSettings.DisableYellowFilter
|
? 'bg-green-600'
|
||||||
? 'bg-green-600'
|
: 'bg-gray-200 dark:bg-gray-700'
|
||||||
: 'bg-gray-200 dark:bg-gray-700'
|
} ${isUpstashStorage ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||||
} ${isUpstashStorage ? 'opacity-50 cursor-not-allowed' : ''}`}
|
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${siteSettings.DisableYellowFilter
|
||||||
siteSettings.DisableYellowFilter
|
? 'translate-x-6'
|
||||||
? 'translate-x-6'
|
: 'translate-x-1'
|
||||||
: 'translate-x-1'
|
}`}
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1900,11 +1956,10 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
|
|||||||
<button
|
<button
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
className={`px-4 py-2 ${
|
className={`px-4 py-2 ${saving
|
||||||
saving
|
? 'bg-gray-400 cursor-not-allowed'
|
||||||
? 'bg-gray-400 cursor-not-allowed'
|
: 'bg-green-600 hover:bg-green-700'
|
||||||
: 'bg-green-600 hover:bg-green-700'
|
} text-white rounded-lg transition-colors`}
|
||||||
} text-white rounded-lg transition-colors`}
|
|
||||||
>
|
>
|
||||||
{saving ? '保存中…' : '保存'}
|
{saving ? '保存中…' : '保存'}
|
||||||
</button>
|
</button>
|
||||||
@@ -1923,6 +1978,7 @@ function AdminPageClient() {
|
|||||||
videoSource: false,
|
videoSource: false,
|
||||||
siteConfig: false,
|
siteConfig: false,
|
||||||
categoryConfig: false,
|
categoryConfig: false,
|
||||||
|
configFile: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 获取管理员配置
|
// 获取管理员配置
|
||||||
@@ -2036,6 +2092,21 @@ function AdminPageClient() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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
|
<CollapsibleTab
|
||||||
title='站点配置'
|
title='站点配置'
|
||||||
|
|||||||
88
src/app/api/admin/config_file/route.ts
Normal file
88
src/app/api/admin/config_file/route.ts
Normal 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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/app/api/config/custom_category/route.ts
Normal file
13
src/app/api/config/custom_category/route.ts
Normal 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);
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import { Suspense } from 'react';
|
|||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { GetBangumiCalendarData } from '@/lib/bangumi.client';
|
import { GetBangumiCalendarData } from '@/lib/bangumi.client';
|
||||||
|
import { getCustomCategories } from '@/lib/config.client';
|
||||||
import {
|
import {
|
||||||
getDoubanCategories,
|
getDoubanCategories,
|
||||||
getDoubanList,
|
getDoubanList,
|
||||||
@@ -80,10 +81,9 @@ function DoubanPageClient() {
|
|||||||
|
|
||||||
// 获取自定义分类数据
|
// 获取自定义分类数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const runtimeConfig = (window as any).RUNTIME_CONFIG;
|
getCustomCategories().then((categories) => {
|
||||||
if (runtimeConfig?.CUSTOM_CATEGORIES?.length > 0) {
|
setCustomCategories(categories);
|
||||||
setCustomCategories(runtimeConfig.CUSTOM_CATEGORIES);
|
});
|
||||||
}
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 同步最新参数值到 ref
|
// 同步最新参数值到 ref
|
||||||
@@ -214,7 +214,7 @@ function DoubanPageClient() {
|
|||||||
snapshot1.selectedWeekday === snapshot2.selectedWeekday &&
|
snapshot1.selectedWeekday === snapshot2.selectedWeekday &&
|
||||||
snapshot1.currentPage === snapshot2.currentPage &&
|
snapshot1.currentPage === snapshot2.currentPage &&
|
||||||
JSON.stringify(snapshot1.multiLevelSelection) ===
|
JSON.stringify(snapshot1.multiLevelSelection) ===
|
||||||
JSON.stringify(snapshot2.multiLevelSelection)
|
JSON.stringify(snapshot2.multiLevelSelection)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
@@ -686,12 +686,12 @@ function DoubanPageClient() {
|
|||||||
return type === 'movie'
|
return type === 'movie'
|
||||||
? '电影'
|
? '电影'
|
||||||
: type === 'tv'
|
: type === 'tv'
|
||||||
? '电视剧'
|
? '电视剧'
|
||||||
: type === 'anime'
|
: type === 'anime'
|
||||||
? '动漫'
|
? '动漫'
|
||||||
: type === 'show'
|
: type === 'show'
|
||||||
? '综艺'
|
? '综艺'
|
||||||
: '自定义';
|
: '自定义';
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPageDescription = () => {
|
const getPageDescription = () => {
|
||||||
@@ -757,24 +757,24 @@ function DoubanPageClient() {
|
|||||||
<div className='justify-start grid grid-cols-3 gap-x-2 gap-y-12 px-0 sm:px-2 sm:grid-cols-[repeat(auto-fill,minmax(160px,1fr))] sm:gap-x-8 sm:gap-y-20'>
|
<div className='justify-start grid grid-cols-3 gap-x-2 gap-y-12 px-0 sm:px-2 sm:grid-cols-[repeat(auto-fill,minmax(160px,1fr))] sm:gap-x-8 sm:gap-y-20'>
|
||||||
{loading || !selectorsReady
|
{loading || !selectorsReady
|
||||||
? // 显示骨架屏
|
? // 显示骨架屏
|
||||||
skeletonData.map((index) => <DoubanCardSkeleton key={index} />)
|
skeletonData.map((index) => <DoubanCardSkeleton key={index} />)
|
||||||
: // 显示实际数据
|
: // 显示实际数据
|
||||||
doubanData.map((item, index) => (
|
doubanData.map((item, index) => (
|
||||||
<div key={`${item.title}-${index}`} className='w-full'>
|
<div key={`${item.title}-${index}`} className='w-full'>
|
||||||
<VideoCard
|
<VideoCard
|
||||||
from='douban'
|
from='douban'
|
||||||
title={item.title}
|
title={item.title}
|
||||||
poster={item.poster}
|
poster={item.poster}
|
||||||
douban_id={Number(item.id)}
|
douban_id={Number(item.id)}
|
||||||
rate={item.rate}
|
rate={item.rate}
|
||||||
year={item.year}
|
year={item.year}
|
||||||
type={type === 'movie' ? 'movie' : ''} // 电影类型严格控制,tv 不控
|
type={type === 'movie' ? 'movie' : ''} // 电影类型严格控制,tv 不控
|
||||||
isBangumi={
|
isBangumi={
|
||||||
type === 'anime' && primarySelection === '每日放送'
|
type === 'anime' && primarySelection === '每日放送'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 加载更多指示器 */}
|
{/* 加载更多指示器 */}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import './globals.css';
|
|||||||
import 'sweetalert2/dist/sweetalert2.min.css';
|
import 'sweetalert2/dist/sweetalert2.min.css';
|
||||||
|
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
import RuntimeConfig from '@/lib/runtime';
|
|
||||||
|
|
||||||
import { GlobalErrorIndicator } from '../components/GlobalErrorIndicator';
|
import { GlobalErrorIndicator } from '../components/GlobalErrorIndicator';
|
||||||
import { SiteProvider } from '../components/SiteProvider';
|
import { SiteProvider } from '../components/SiteProvider';
|
||||||
@@ -39,6 +38,8 @@ export default async function RootLayout({
|
|||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
|
const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage';
|
||||||
|
|
||||||
let siteName = process.env.NEXT_PUBLIC_SITE_NAME || 'MoonTV';
|
let siteName = process.env.NEXT_PUBLIC_SITE_NAME || 'MoonTV';
|
||||||
let announcement =
|
let announcement =
|
||||||
process.env.ANNOUNCEMENT ||
|
process.env.ANNOUNCEMENT ||
|
||||||
@@ -51,13 +52,7 @@ export default async function RootLayout({
|
|||||||
let doubanImageProxy = process.env.NEXT_PUBLIC_DOUBAN_IMAGE_PROXY || '';
|
let doubanImageProxy = process.env.NEXT_PUBLIC_DOUBAN_IMAGE_PROXY || '';
|
||||||
let disableYellowFilter =
|
let disableYellowFilter =
|
||||||
process.env.NEXT_PUBLIC_DISABLE_YELLOW_FILTER === 'true';
|
process.env.NEXT_PUBLIC_DISABLE_YELLOW_FILTER === 'true';
|
||||||
let customCategories =
|
if (storageType !== 'upstash' && storageType !== 'localstorage') {
|
||||||
(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') {
|
|
||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
siteName = config.SiteConfig.SiteName;
|
siteName = config.SiteConfig.SiteName;
|
||||||
announcement = config.SiteConfig.Announcement;
|
announcement = config.SiteConfig.Announcement;
|
||||||
@@ -67,13 +62,6 @@ export default async function RootLayout({
|
|||||||
doubanImageProxyType = config.SiteConfig.DoubanImageProxyType;
|
doubanImageProxyType = config.SiteConfig.DoubanImageProxyType;
|
||||||
doubanImageProxy = config.SiteConfig.DoubanImageProxy;
|
doubanImageProxy = config.SiteConfig.DoubanImageProxy;
|
||||||
disableYellowFilter = config.SiteConfig.DisableYellowFilter;
|
disableYellowFilter = config.SiteConfig.DisableYellowFilter;
|
||||||
customCategories = config.CustomCategories.filter(
|
|
||||||
(category) => !category.disabled
|
|
||||||
).map((category) => ({
|
|
||||||
name: category.name || '',
|
|
||||||
type: category.type,
|
|
||||||
query: category.query,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将运行时配置注入到全局 window 对象,供客户端在运行时读取
|
// 将运行时配置注入到全局 window 对象,供客户端在运行时读取
|
||||||
@@ -85,7 +73,6 @@ export default async function RootLayout({
|
|||||||
DOUBAN_IMAGE_PROXY_TYPE: doubanImageProxyType,
|
DOUBAN_IMAGE_PROXY_TYPE: doubanImageProxyType,
|
||||||
DOUBAN_IMAGE_PROXY: doubanImageProxy,
|
DOUBAN_IMAGE_PROXY: doubanImageProxy,
|
||||||
DISABLE_YELLOW_FILTER: disableYellowFilter,
|
DISABLE_YELLOW_FILTER: disableYellowFilter,
|
||||||
CUSTOM_CATEGORIES: customCategories,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import Link from 'next/link';
|
|||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { getCustomCategories } from '@/lib/config.client';
|
||||||
|
|
||||||
interface MobileBottomNavProps {
|
interface MobileBottomNavProps {
|
||||||
/**
|
/**
|
||||||
* 主动指定当前激活的路径。当未提供时,自动使用 usePathname() 获取的路径。
|
* 主动指定当前激活的路径。当未提供时,自动使用 usePathname() 获取的路径。
|
||||||
@@ -46,17 +48,18 @@ const MobileBottomNav = ({ activePath }: MobileBottomNavProps) => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const runtimeConfig = (window as any).RUNTIME_CONFIG;
|
getCustomCategories().then((categories) => {
|
||||||
if (runtimeConfig?.CUSTOM_CATEGORIES?.length > 0) {
|
if (categories.length > 0) {
|
||||||
setNavItems((prevItems) => [
|
setNavItems((prevItems) => [
|
||||||
...prevItems,
|
...prevItems,
|
||||||
{
|
{
|
||||||
icon: Star,
|
icon: Star,
|
||||||
label: '自定义',
|
label: '自定义',
|
||||||
href: '/douban?type=custom',
|
href: '/douban?type=custom',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const isActive = (href: string) => {
|
const isActive = (href: string) => {
|
||||||
@@ -97,11 +100,10 @@ const MobileBottomNav = ({ activePath }: MobileBottomNavProps) => {
|
|||||||
className='flex flex-col items-center justify-center w-full h-14 gap-1 text-xs'
|
className='flex flex-col items-center justify-center w-full h-14 gap-1 text-xs'
|
||||||
>
|
>
|
||||||
<item.icon
|
<item.icon
|
||||||
className={`h-6 w-6 ${
|
className={`h-6 w-6 ${active
|
||||||
active
|
|
||||||
? 'text-green-600 dark:text-green-400'
|
? 'text-green-600 dark:text-green-400'
|
||||||
: 'text-gray-500 dark:text-gray-400'
|
: 'text-gray-500 dark:text-gray-400'
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
className={
|
className={
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
|
import { getCustomCategories } from '@/lib/config.client';
|
||||||
|
|
||||||
import { useSite } from './SiteProvider';
|
import { useSite } from './SiteProvider';
|
||||||
|
|
||||||
interface SidebarContextType {
|
interface SidebarContextType {
|
||||||
@@ -148,17 +150,18 @@ const Sidebar = ({ onToggle, activePath = '/' }: SidebarProps) => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const runtimeConfig = (window as any).RUNTIME_CONFIG;
|
getCustomCategories().then((categories) => {
|
||||||
if (runtimeConfig?.CUSTOM_CATEGORIES?.length > 0) {
|
if (categories.length > 0) {
|
||||||
setMenuItems((prevItems) => [
|
setMenuItems((prevItems) => [
|
||||||
...prevItems,
|
...prevItems,
|
||||||
{
|
{
|
||||||
icon: Star,
|
icon: Star,
|
||||||
label: '自定义',
|
label: '自定义',
|
||||||
href: '/douban?type=custom',
|
href: '/douban?type=custom',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -167,9 +170,8 @@ const Sidebar = ({ onToggle, activePath = '/' }: SidebarProps) => {
|
|||||||
<div className='hidden md:flex'>
|
<div className='hidden md:flex'>
|
||||||
<aside
|
<aside
|
||||||
data-sidebar
|
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 ${
|
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'
|
||||||
isCollapsed ? 'w-16' : 'w-64'
|
}`}
|
||||||
}`}
|
|
||||||
style={{
|
style={{
|
||||||
backdropFilter: 'blur(20px)',
|
backdropFilter: 'blur(20px)',
|
||||||
WebkitBackdropFilter: 'blur(20px)',
|
WebkitBackdropFilter: 'blur(20px)',
|
||||||
@@ -179,9 +181,8 @@ const Sidebar = ({ onToggle, activePath = '/' }: SidebarProps) => {
|
|||||||
{/* 顶部 Logo 区域 */}
|
{/* 顶部 Logo 区域 */}
|
||||||
<div className='relative h-16'>
|
<div className='relative h-16'>
|
||||||
<div
|
<div
|
||||||
className={`absolute inset-0 flex items-center justify-center transition-opacity duration-200 ${
|
className={`absolute inset-0 flex items-center justify-center transition-opacity duration-200 ${isCollapsed ? 'opacity-0' : 'opacity-100'
|
||||||
isCollapsed ? 'opacity-0' : 'opacity-100'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<div className='w-[calc(100%-4rem)] flex justify-center'>
|
<div className='w-[calc(100%-4rem)] flex justify-center'>
|
||||||
{!isCollapsed && <Logo />}
|
{!isCollapsed && <Logo />}
|
||||||
@@ -189,9 +190,8 @@ const Sidebar = ({ onToggle, activePath = '/' }: SidebarProps) => {
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={handleToggle}
|
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 ${
|
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'
|
||||||
isCollapsed ? 'left-1/2 -translate-x-1/2' : 'right-2'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<Menu className='h-4 w-4' />
|
<Menu className='h-4 w-4' />
|
||||||
</button>
|
</button>
|
||||||
@@ -203,9 +203,8 @@ const Sidebar = ({ onToggle, activePath = '/' }: SidebarProps) => {
|
|||||||
href='/'
|
href='/'
|
||||||
onClick={() => setActive('/')}
|
onClick={() => setActive('/')}
|
||||||
data-active={active === '/'}
|
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 ${
|
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'
|
||||||
isCollapsed ? 'w-full max-w-none mx-0' : 'mx-0'
|
} gap-3 justify-start`}
|
||||||
} gap-3 justify-start`}
|
|
||||||
>
|
>
|
||||||
<div className='w-4 h-4 flex items-center justify-center'>
|
<div className='w-4 h-4 flex items-center justify-center'>
|
||||||
<Home className='h-4 w-4 text-gray-500 group-hover:text-green-600 data-[active=true]:text-green-700 dark:text-gray-400 dark:group-hover:text-green-400 dark:data-[active=true]:text-green-400' />
|
<Home className='h-4 w-4 text-gray-500 group-hover:text-green-600 data-[active=true]:text-green-700 dark:text-gray-400 dark:group-hover:text-green-400 dark:data-[active=true]:text-green-400' />
|
||||||
@@ -224,9 +223,8 @@ const Sidebar = ({ onToggle, activePath = '/' }: SidebarProps) => {
|
|||||||
setActive('/search');
|
setActive('/search');
|
||||||
}}
|
}}
|
||||||
data-active={active === '/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 ${
|
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'
|
||||||
isCollapsed ? 'w-full max-w-none mx-0' : 'mx-0'
|
} gap-3 justify-start`}
|
||||||
} gap-3 justify-start`}
|
|
||||||
>
|
>
|
||||||
<div className='w-4 h-4 flex items-center justify-center'>
|
<div className='w-4 h-4 flex items-center justify-center'>
|
||||||
<Search className='h-4 w-4 text-gray-500 group-hover:text-green-600 data-[active=true]:text-green-700 dark:text-gray-400 dark:group-hover:text-green-400 dark:data-[active=true]:text-green-400' />
|
<Search className='h-4 w-4 text-gray-500 group-hover:text-green-600 data-[active=true]:text-green-700 dark:text-gray-400 dark:group-hover:text-green-400 dark:data-[active=true]:text-green-400' />
|
||||||
@@ -261,9 +259,8 @@ const Sidebar = ({ onToggle, activePath = '/' }: SidebarProps) => {
|
|||||||
href={item.href}
|
href={item.href}
|
||||||
onClick={() => setActive(item.href)}
|
onClick={() => setActive(item.href)}
|
||||||
data-active={isActive}
|
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 ${
|
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'
|
||||||
isCollapsed ? 'w-full max-w-none mx-0' : 'mx-0'
|
} gap-3 justify-start`}
|
||||||
} gap-3 justify-start`}
|
|
||||||
>
|
>
|
||||||
<div className='w-4 h-4 flex items-center justify-center'>
|
<div className='w-4 h-4 flex items-center justify-center'>
|
||||||
<Icon className='h-4 w-4 text-gray-500 group-hover:text-green-600 data-[active=true]:text-green-700 dark:text-gray-400 dark:group-hover:text-green-400 dark:data-[active=true]:text-green-400' />
|
<Icon className='h-4 w-4 text-gray-500 group-hover:text-green-600 data-[active=true]:text-green-700 dark:text-gray-400 dark:group-hover:text-green-400 dark:data-[active=true]:text-green-400' />
|
||||||
@@ -281,9 +278,8 @@ const Sidebar = ({ onToggle, activePath = '/' }: SidebarProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
<div
|
<div
|
||||||
className={`transition-all duration-300 sidebar-offset ${
|
className={`transition-all duration-300 sidebar-offset ${isCollapsed ? 'w-16' : 'w-64'
|
||||||
isCollapsed ? 'w-16' : 'w-64'
|
}`}
|
||||||
}`}
|
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</SidebarContext.Provider>
|
</SidebarContext.Provider>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export interface AdminConfig {
|
export interface AdminConfig {
|
||||||
|
ConfigFile: string;
|
||||||
SiteConfig: {
|
SiteConfig: {
|
||||||
SiteName: string;
|
SiteName: string;
|
||||||
Announcement: string;
|
Announcement: string;
|
||||||
|
|||||||
17
src/lib/config.client.ts
Normal file
17
src/lib/config.client.ts
Normal 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,
|
||||||
|
}));
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@
|
|||||||
import { getStorage } from '@/lib/db';
|
import { getStorage } from '@/lib/db';
|
||||||
|
|
||||||
import { AdminConfig } from './admin.types';
|
import { AdminConfig } from './admin.types';
|
||||||
import runtimeConfig from './runtime';
|
|
||||||
|
|
||||||
export interface ApiSite {
|
export interface ApiSite {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -14,7 +13,7 @@ export interface ApiSite {
|
|||||||
|
|
||||||
interface ConfigFileStruct {
|
interface ConfigFileStruct {
|
||||||
cache_time?: number;
|
cache_time?: number;
|
||||||
api_site: {
|
api_site?: {
|
||||||
[key: string]: ApiSite;
|
[key: string]: ApiSite;
|
||||||
};
|
};
|
||||||
custom_category?: {
|
custom_category?: {
|
||||||
@@ -48,244 +47,267 @@ export const API_CONFIG = {
|
|||||||
let fileConfig: ConfigFileStruct;
|
let fileConfig: ConfigFileStruct;
|
||||||
let cachedConfig: AdminConfig;
|
let cachedConfig: AdminConfig;
|
||||||
|
|
||||||
async function initConfig() {
|
export function refineConfig(adminConfig: AdminConfig): AdminConfig {
|
||||||
if (cachedConfig) {
|
try {
|
||||||
return;
|
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])
|
||||||
|
);
|
||||||
|
|
||||||
if (process.env.DOCKER_ENV === 'true') {
|
apiSiteEntries.forEach(([key, site]) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
const existingSource = sourceConfigMap.get(key);
|
||||||
const _require = eval('require') as NodeRequire;
|
if (existingSource) {
|
||||||
const fs = _require('fs') as typeof import('fs');
|
// 如果已存在,只覆盖 name、api、detail 和 from
|
||||||
const path = _require('path') as typeof import('path');
|
existingSource.name = site.name;
|
||||||
|
existingSource.api = site.api;
|
||||||
const configPath = path.join(process.cwd(), 'config.json');
|
existingSource.detail = site.detail;
|
||||||
const raw = fs.readFileSync(configPath, 'utf-8');
|
existingSource.from = 'config';
|
||||||
fileConfig = JSON.parse(raw) as ConfigFileStruct;
|
} else {
|
||||||
console.log('load dynamic config success');
|
// 如果不存在,创建新条目
|
||||||
} else {
|
sourceConfigMap.set(key, {
|
||||||
// 默认使用编译时生成的配置
|
|
||||||
fileConfig = runtimeConfig as unknown as ConfigFileStruct;
|
|
||||||
}
|
|
||||||
const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage';
|
|
||||||
if (storageType !== 'localstorage') {
|
|
||||||
// 数据库存储,读取并补全管理员配置
|
|
||||||
const storage = getStorage();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 尝试从数据库获取管理员配置
|
|
||||||
let adminConfig: AdminConfig | null = null;
|
|
||||||
if (storage && typeof (storage as any).getAdminConfig === 'function') {
|
|
||||||
adminConfig = await (storage as any).getAdminConfig();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取所有用户名,用于补全 Users
|
|
||||||
let userNames: string[] = [];
|
|
||||||
if (storage && typeof (storage as any).getAllUsers === 'function') {
|
|
||||||
try {
|
|
||||||
userNames = await (storage as any).getAllUsers();
|
|
||||||
} catch (e) {
|
|
||||||
console.error('获取用户列表失败:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从文件中获取源信息,用于补全源
|
|
||||||
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])
|
|
||||||
);
|
|
||||||
|
|
||||||
apiSiteEntries.forEach(([key, site]) => {
|
|
||||||
sourceConfigMap.set(key, {
|
|
||||||
key,
|
|
||||||
name: site.name,
|
|
||||||
api: site.api,
|
|
||||||
detail: site.detail,
|
|
||||||
from: 'config',
|
|
||||||
disabled: false,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 将 Map 转换回数组
|
|
||||||
adminConfig.SourceConfig = Array.from(sourceConfigMap.values());
|
|
||||||
|
|
||||||
// 检查现有源是否在 fileConfig.api_site 中,如果不在则标记为 custom
|
|
||||||
const apiSiteKeys = new Set(apiSiteEntries.map(([key]) => key));
|
|
||||||
adminConfig.SourceConfig.forEach((source) => {
|
|
||||||
if (!apiSiteKeys.has(source.key)) {
|
|
||||||
source.from = 'custom';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 确保 CustomCategories 被初始化
|
|
||||||
if (!adminConfig.CustomCategories) {
|
|
||||||
adminConfig.CustomCategories = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 补全 CustomCategories
|
|
||||||
const customCategoriesMap = new Map(
|
|
||||||
adminConfig.CustomCategories.map((c) => [c.query + c.type, c])
|
|
||||||
);
|
|
||||||
|
|
||||||
customCategories.forEach((category) => {
|
|
||||||
customCategoriesMap.set(category.query + category.type, {
|
|
||||||
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());
|
|
||||||
|
|
||||||
const existedUsers = new Set(
|
|
||||||
(adminConfig.UserConfig.Users || []).map((u) => u.username)
|
|
||||||
);
|
|
||||||
userNames.forEach((uname) => {
|
|
||||||
if (!existedUsers.has(uname)) {
|
|
||||||
adminConfig!.UserConfig.Users.push({
|
|
||||||
username: uname,
|
|
||||||
role: 'user',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// 站长
|
|
||||||
const ownerUser = process.env.USERNAME;
|
|
||||||
if (ownerUser) {
|
|
||||||
adminConfig!.UserConfig.Users = adminConfig!.UserConfig.Users.filter(
|
|
||||||
(u) => u.username !== ownerUser
|
|
||||||
);
|
|
||||||
adminConfig!.UserConfig.Users.unshift({
|
|
||||||
username: ownerUser,
|
|
||||||
role: 'owner',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 数据库中没有配置,创建新的管理员配置
|
|
||||||
let allUsers = userNames.map((uname) => ({
|
|
||||||
username: uname,
|
|
||||||
role: 'user',
|
|
||||||
}));
|
|
||||||
const ownerUser = process.env.USERNAME;
|
|
||||||
if (ownerUser) {
|
|
||||||
allUsers = allUsers.filter((u) => u.username !== ownerUser);
|
|
||||||
allUsers.unshift({
|
|
||||||
username: ownerUser,
|
|
||||||
role: 'owner',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
adminConfig = {
|
|
||||||
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: 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,
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 写回数据库(更新/创建)
|
|
||||||
if (storage && typeof (storage as any).setAdminConfig === 'function') {
|
|
||||||
await (storage as any).setAdminConfig(adminConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新缓存
|
|
||||||
cachedConfig = adminConfig;
|
|
||||||
} 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,
|
key,
|
||||||
name: site.name,
|
name: site.name,
|
||||||
api: site.api,
|
api: site.api,
|
||||||
detail: site.detail,
|
detail: site.detail,
|
||||||
from: 'config',
|
from: 'config',
|
||||||
disabled: false,
|
disabled: false,
|
||||||
})),
|
});
|
||||||
CustomCategories:
|
}
|
||||||
fileConfig.custom_category?.map((category) => ({
|
});
|
||||||
|
|
||||||
|
// 检查现有源是否在 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数据库存储,读取并补全管理员配置
|
||||||
|
const storage = getStorage();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 尝试从数据库获取管理员配置
|
||||||
|
let adminConfig: AdminConfig | null = null;
|
||||||
|
if (storage && typeof (storage as any).getAdminConfig === 'function') {
|
||||||
|
adminConfig = await (storage as any).getAdminConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有用户名,用于补全 Users
|
||||||
|
let userNames: string[] = [];
|
||||||
|
if (storage && typeof (storage as any).getAllUsers === 'function') {
|
||||||
|
try {
|
||||||
|
userNames = await (storage as any).getAllUsers();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('获取用户列表失败:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 || [];
|
||||||
|
|
||||||
|
// 补全 SourceConfig
|
||||||
|
const sourceConfigMap = new Map(
|
||||||
|
(adminConfig.SourceConfig || []).map((s) => [s.key, s])
|
||||||
|
);
|
||||||
|
|
||||||
|
apiSiteEntries.forEach(([key, site]) => {
|
||||||
|
sourceConfigMap.set(key, {
|
||||||
|
key,
|
||||||
|
name: site.name,
|
||||||
|
api: site.api,
|
||||||
|
detail: site.detail,
|
||||||
|
from: 'config',
|
||||||
|
disabled: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 将 Map 转换回数组
|
||||||
|
adminConfig.SourceConfig = Array.from(sourceConfigMap.values());
|
||||||
|
|
||||||
|
// 检查现有源是否在 fileConfig.api_site 中,如果不在则标记为 custom
|
||||||
|
const apiSiteKeys = new Set(apiSiteEntries.map(([key]) => key));
|
||||||
|
adminConfig.SourceConfig.forEach((source) => {
|
||||||
|
if (!apiSiteKeys.has(source.key)) {
|
||||||
|
source.from = 'custom';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 确保 CustomCategories 被初始化
|
||||||
|
if (!adminConfig.CustomCategories) {
|
||||||
|
adminConfig.CustomCategories = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 补全 CustomCategories
|
||||||
|
const customCategoriesMap = new Map(
|
||||||
|
adminConfig.CustomCategories.map((c) => [c.query + c.type, c])
|
||||||
|
);
|
||||||
|
|
||||||
|
customCategories.forEach((category) => {
|
||||||
|
customCategoriesMap.set(category.query + category.type, {
|
||||||
name: category.name,
|
name: category.name,
|
||||||
type: category.type,
|
type: category.type,
|
||||||
query: category.query,
|
query: category.query,
|
||||||
from: 'config',
|
from: 'config',
|
||||||
disabled: false,
|
disabled: false,
|
||||||
})) || [],
|
});
|
||||||
} as AdminConfig;
|
});
|
||||||
|
|
||||||
|
// 检查现有 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());
|
||||||
|
|
||||||
|
const existedUsers = new Set(
|
||||||
|
(adminConfig.UserConfig.Users || []).map((u) => u.username)
|
||||||
|
);
|
||||||
|
userNames.forEach((uname) => {
|
||||||
|
if (!existedUsers.has(uname)) {
|
||||||
|
adminConfig!.UserConfig.Users.push({
|
||||||
|
username: uname,
|
||||||
|
role: 'user',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 站长
|
||||||
|
const ownerUser = process.env.USERNAME;
|
||||||
|
if (ownerUser) {
|
||||||
|
adminConfig!.UserConfig.Users = adminConfig!.UserConfig.Users.filter(
|
||||||
|
(u) => u.username !== ownerUser
|
||||||
|
);
|
||||||
|
adminConfig!.UserConfig.Users.unshift({
|
||||||
|
username: ownerUser,
|
||||||
|
role: 'owner',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fileConfig = {} as ConfigFileStruct;
|
||||||
|
// 数据库中没有配置,创建新的管理员配置
|
||||||
|
let allUsers = userNames.map((uname) => ({
|
||||||
|
username: uname,
|
||||||
|
role: 'user',
|
||||||
|
}));
|
||||||
|
const ownerUser = process.env.USERNAME;
|
||||||
|
if (ownerUser) {
|
||||||
|
allUsers = allUsers.filter((u) => u.username !== ownerUser);
|
||||||
|
allUsers.unshift({
|
||||||
|
username: ownerUser,
|
||||||
|
role: 'owner',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
adminConfig = {
|
||||||
|
ConfigFile: '',
|
||||||
|
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: allUsers as any,
|
||||||
|
},
|
||||||
|
SourceConfig: [],
|
||||||
|
CustomCategories: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写回数据库(更新/创建)
|
||||||
|
if (storage && typeof (storage as any).setAdminConfig === 'function') {
|
||||||
|
await (storage as any).setAdminConfig(adminConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新缓存
|
||||||
|
cachedConfig = adminConfig;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('加载管理员配置失败:', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getConfig(): Promise<AdminConfig> {
|
export async function getConfig(): Promise<AdminConfig> {
|
||||||
const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage';
|
if (process.env.DOCKER_ENV === 'true') {
|
||||||
if (process.env.DOCKER_ENV === 'true' || storageType === 'localstorage') {
|
|
||||||
await initConfig();
|
await initConfig();
|
||||||
return cachedConfig;
|
return cachedConfig;
|
||||||
}
|
}
|
||||||
@@ -320,9 +342,15 @@ export async function getConfig(): Promise<AdminConfig> {
|
|||||||
adminConfig.SiteConfig.DisableYellowFilter =
|
adminConfig.SiteConfig.DisableYellowFilter =
|
||||||
process.env.NEXT_PUBLIC_DISABLE_YELLOW_FILTER === 'true';
|
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(
|
const sourceConfigMap = new Map(
|
||||||
(adminConfig.SourceConfig || []).map((s) => [s.key, s])
|
(adminConfig.SourceConfig || []).map((s) => [s.key, s])
|
||||||
);
|
);
|
||||||
@@ -398,8 +426,19 @@ export async function getConfig(): Promise<AdminConfig> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function resetConfig() {
|
export async function resetConfig() {
|
||||||
const storageType = process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage';
|
let originConfig: AdminConfig | null = null;
|
||||||
const storage = getStorage();
|
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
|
// 获取所有用户名,用于补全 Users
|
||||||
let userNames: string[] = [];
|
let userNames: string[] = [];
|
||||||
if (storage && typeof (storage as any).getAllUsers === 'function') {
|
if (storage && typeof (storage as any).getAllUsers === 'function') {
|
||||||
@@ -410,22 +449,7 @@ export async function resetConfig() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.DOCKER_ENV === 'true') {
|
const apiSiteEntries = Object.entries(fileConfig.api_site || []);
|
||||||
// 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 customCategories = fileConfig.custom_category || [];
|
const customCategories = fileConfig.custom_category || [];
|
||||||
let allUsers = userNames.map((uname) => ({
|
let allUsers = userNames.map((uname) => ({
|
||||||
username: uname,
|
username: uname,
|
||||||
@@ -440,6 +464,7 @@ export async function resetConfig() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
const adminConfig = {
|
const adminConfig = {
|
||||||
|
ConfigFile: originConfig?.ConfigFile || '',
|
||||||
SiteConfig: {
|
SiteConfig: {
|
||||||
SiteName: process.env.NEXT_PUBLIC_SITE_NAME || 'MoonTV',
|
SiteName: process.env.NEXT_PUBLIC_SITE_NAME || 'MoonTV',
|
||||||
Announcement:
|
Announcement:
|
||||||
@@ -471,12 +496,12 @@ export async function resetConfig() {
|
|||||||
CustomCategories:
|
CustomCategories:
|
||||||
storageType === 'redis'
|
storageType === 'redis'
|
||||||
? customCategories?.map((category) => ({
|
? customCategories?.map((category) => ({
|
||||||
name: category.name,
|
name: category.name,
|
||||||
type: category.type,
|
type: category.type,
|
||||||
query: category.query,
|
query: category.query,
|
||||||
from: 'config',
|
from: 'config',
|
||||||
disabled: false,
|
disabled: false,
|
||||||
})) || []
|
})) || []
|
||||||
: [],
|
: [],
|
||||||
} as AdminConfig;
|
} as AdminConfig;
|
||||||
|
|
||||||
@@ -487,10 +512,11 @@ export async function resetConfig() {
|
|||||||
// serverless 环境,直接使用 adminConfig
|
// serverless 环境,直接使用 adminConfig
|
||||||
cachedConfig = adminConfig;
|
cachedConfig = adminConfig;
|
||||||
}
|
}
|
||||||
|
cachedConfig.ConfigFile = adminConfig.ConfigFile;
|
||||||
cachedConfig.SiteConfig = adminConfig.SiteConfig;
|
cachedConfig.SiteConfig = adminConfig.SiteConfig;
|
||||||
cachedConfig.UserConfig = adminConfig.UserConfig;
|
cachedConfig.UserConfig = adminConfig.UserConfig;
|
||||||
cachedConfig.SourceConfig = adminConfig.SourceConfig;
|
cachedConfig.SourceConfig = adminConfig.SourceConfig;
|
||||||
cachedConfig.CustomCategories = adminConfig.CustomCategories;
|
cachedConfig.CustomCategories = adminConfig.CustomCategories || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCacheTime(): Promise<number> {
|
export async function getCacheTime(): Promise<number> {
|
||||||
|
|||||||
Reference in New Issue
Block a user