添加自定义主题支持

This commit is contained in:
mtvpls
2025-12-12 18:06:34 +08:00
parent 4ff7c1caf3
commit 15d86e3252
7 changed files with 918 additions and 1 deletions
+333
View File
@@ -32,6 +32,7 @@ import {
ExternalLink,
FileText,
FolderOpen,
Palette,
Settings,
Tv,
Users,
@@ -4235,6 +4236,322 @@ const ConfigFileComponent = ({
);
};
// 主题配置组件
const ThemeConfigComponent = ({
config,
refreshConfig,
}: {
config: AdminConfig | null;
refreshConfig: () => Promise<void>;
}) => {
const { alertModal, showAlert, hideAlert } = useAlertModal();
const { isLoading, withLoading } = useLoadingState();
const [themeSettings, setThemeSettings] = useState({
enableBuiltInTheme: false,
builtInTheme: 'default',
customCSS: '',
enableCache: true,
cacheMinutes: 1440, // 默认1天(1440分钟)
});
useEffect(() => {
if (config?.ThemeConfig) {
setThemeSettings({
enableBuiltInTheme: config.ThemeConfig.enableBuiltInTheme || false,
builtInTheme: config.ThemeConfig.builtInTheme || 'default',
customCSS: config.ThemeConfig.customCSS || '',
enableCache: config.ThemeConfig.enableCache !== false,
cacheMinutes: config.ThemeConfig.cacheMinutes || 1440,
});
}
}, [config]);
const handleSave = async () => {
await withLoading('saveThemeConfig', async () => {
try {
const response = await fetch('/api/admin/theme', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(themeSettings),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || '保存失败');
}
showAlert({
type: 'success',
title: '保存成功',
message: '主题配置已更新',
timer: 2000,
});
await refreshConfig();
// 刷新页面以应用新主题
setTimeout(() => {
window.location.reload();
}, 1000);
} catch (error) {
showAlert({
type: 'error',
title: '保存失败',
message: (error as Error).message,
});
}
});
};
const builtInThemes = [
{
value: 'default',
label: '默认主题',
color: '#3b82f6',
},
{
value: 'dark_blue',
label: '深蓝夜空',
color: '#3b82f6',
},
{
value: 'purple_dream',
label: '紫色梦境',
color: '#a78bfa',
},
{
value: 'green_forest',
label: '翠绿森林',
color: '#10b981',
},
{
value: 'orange_sunset',
label: '橙色日落',
color: '#f97316',
},
{
value: 'pink_candy',
label: '粉色糖果',
color: '#ec4899',
},
{
value: 'cyan_ocean',
label: '青色海洋',
color: '#06b6d4',
},
];
return (
<div className='space-y-6'>
{/* 主题类型选择 */}
<div className='bg-white dark:bg-gray-800 rounded-lg p-6 border border-gray-200 dark:border-gray-700'>
<h3 className='text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4'>
</h3>
<div className='space-y-4'>
<label className='flex items-center space-x-3 cursor-pointer'>
<input
type='radio'
checked={!themeSettings.enableBuiltInTheme}
onChange={() =>
setThemeSettings((prev) => ({
...prev,
enableBuiltInTheme: false,
}))
}
className='w-4 h-4 text-blue-600'
/>
<span className='text-gray-900 dark:text-gray-100'>
CSS使CSS编辑器
</span>
</label>
<label className='flex items-center space-x-3 cursor-pointer'>
<input
type='radio'
checked={themeSettings.enableBuiltInTheme}
onChange={() =>
setThemeSettings((prev) => ({
...prev,
enableBuiltInTheme: true,
}))
}
className='w-4 h-4 text-blue-600'
/>
<span className='text-gray-900 dark:text-gray-100'>
使
</span>
</label>
</div>
</div>
{/* 内置主题选择 */}
{themeSettings.enableBuiltInTheme && (
<div className='bg-white dark:bg-gray-800 rounded-lg p-6 border border-gray-200 dark:border-gray-700'>
<h3 className='text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4'>
</h3>
<div className='flex flex-wrap gap-3'>
{builtInThemes.map((theme) => (
<div
key={theme.value}
onClick={() =>
setThemeSettings((prev) => ({
...prev,
builtInTheme: theme.value,
}))
}
className={`cursor-pointer rounded-lg border-2 p-3 transition-all hover:shadow-md ${
themeSettings.builtInTheme === theme.value
? 'border-blue-500 ring-2 ring-blue-200 dark:ring-blue-800 bg-blue-50 dark:bg-blue-900/20'
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600'
}`}
>
<div className='flex items-center gap-3'>
{/* 圆形颜色预览 */}
<div
className='w-10 h-10 rounded-full flex-shrink-0 shadow-sm'
style={{ backgroundColor: theme.color }}
/>
{/* 主题名称 */}
<div className='flex items-center gap-2'>
<span className='text-sm font-medium text-gray-900 dark:text-gray-100 whitespace-nowrap'>
{theme.label}
</span>
{themeSettings.builtInTheme === theme.value && (
<div className='w-4 h-4 rounded-full bg-blue-500 flex items-center justify-center flex-shrink-0'>
<svg
className='w-2.5 h-2.5 text-white'
fill='none'
stroke='currentColor'
viewBox='0 0 24 24'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={3}
d='M5 13l4 4L19 7'
/>
</svg>
</div>
)}
</div>
</div>
</div>
))}
</div>
<p className='mt-4 text-sm text-gray-600 dark:text-gray-400'>
CSS将被禁用
</p>
</div>
)}
{/* 自定义CSS编辑器 */}
{!themeSettings.enableBuiltInTheme && (
<div className='bg-white dark:bg-gray-800 rounded-lg p-6 border border-gray-200 dark:border-gray-700'>
<h3 className='text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4'>
CSS
</h3>
<textarea
value={themeSettings.customCSS}
onChange={(e) =>
setThemeSettings((prev) => ({
...prev,
customCSS: e.target.value,
}))
}
placeholder='在此输入自定义CSS代码...'
className='w-full h-96 px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 font-mono text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent'
/>
<p className='mt-2 text-sm text-gray-600 dark:text-gray-400'>
使CSS变量
</p>
</div>
)}
{/* 缓存设置 */}
<div className='bg-white dark:bg-gray-800 rounded-lg p-6 border border-gray-200 dark:border-gray-700'>
<h3 className='text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4'>
</h3>
<div className='space-y-4'>
<label className='flex items-center space-x-3 cursor-pointer'>
<input
type='checkbox'
checked={themeSettings.enableCache}
onChange={(e) =>
setThemeSettings((prev) => ({
...prev,
enableCache: e.target.checked,
}))
}
className='w-4 h-4 text-blue-600 rounded'
/>
<span className='text-gray-900 dark:text-gray-100'>
</span>
</label>
{themeSettings.enableCache && (
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
</label>
<input
type='number'
min='1'
max='43200'
value={themeSettings.cacheMinutes}
onChange={(e) =>
setThemeSettings((prev) => ({
...prev,
cacheMinutes: parseInt(e.target.value) || 1440,
}))
}
className='w-full px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent'
/>
<p className='mt-2 text-sm text-gray-600 dark:text-gray-400'>
60114401100807
</p>
</div>
)}
</div>
<p className='mt-4 text-sm text-gray-600 dark:text-gray-400'>
CSS文件指定时间
</p>
</div>
{/* 保存按钮 */}
<div className='flex justify-end'>
<button
onClick={handleSave}
disabled={isLoading('saveThemeConfig')}
className={
isLoading('saveThemeConfig')
? buttonStyles.disabled
: buttonStyles.success
}
>
{isLoading('saveThemeConfig') ? '保存中...' : '保存主题配置'}
</button>
</div>
{/* 弹窗 */}
<AlertModal
isOpen={alertModal.isOpen}
onClose={hideAlert}
type={alertModal.type}
title={alertModal.title}
message={alertModal.message}
timer={alertModal.timer}
showConfirm={alertModal.showConfirm}
/>
</div>
);
};
// 新增站点配置组件
const SiteConfigComponent = ({
config,
@@ -5869,6 +6186,7 @@ function AdminPageClient() {
configFile: false,
dataMigration: false,
customAdFilter: false,
themeConfig: false,
});
// 获取管理员配置
@@ -6016,6 +6334,21 @@ function AdminPageClient() {
<SiteConfigComponent config={config} refreshConfig={fetchConfig} />
</CollapsibleTab>
{/* 主题配置标签 */}
<CollapsibleTab
title='主题配置'
icon={
<Palette
size={20}
className='text-gray-600 dark:text-gray-400'
/>
}
isExpanded={expandedTabs.themeConfig}
onToggle={() => toggleTab('themeConfig')}
>
<ThemeConfigComponent config={config} refreshConfig={fetchConfig} />
</CollapsibleTab>
<div className='space-y-4'>
{/* 用户配置标签 */}
<CollapsibleTab