mirror of https://github.com/djteang/OrangeTV.git
Compare commits
10 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
2194a3d6ad | |
|
|
3fd6211697 | |
|
|
7357131005 | |
|
|
49d1d3b8b8 | |
|
|
668146f414 | |
|
|
b07b4ef36a | |
|
|
7707ba5414 | |
|
|
3457a7c565 | |
|
|
c462e1c2b5 | |
|
|
a5e9ce41f1 |
|
|
@ -26,7 +26,7 @@
|
|||
- ❤️ **收藏 + 继续观看**:支持 Kvrocks/Redis/Upstash 存储,多端同步进度。
|
||||
- 📱 **PWA**:离线缓存、安装到桌面/主屏,移动端原生体验。
|
||||
- 🌗 **响应式布局**:桌面侧边栏 + 移动底部导航,自适应各种屏幕尺寸。
|
||||
- 👿 **智能去广告**:自动跳过视频中的切片广告(实验性)。
|
||||
- 👿 **智能去广告**:自动跳过视频中的切片广告(实验性)。
|
||||
|
||||
### 注意:部署后项目为空壳项目,无内置播放源和直播源,需要自行收集
|
||||
|
||||
|
|
@ -77,6 +77,7 @@ services:
|
|||
restart: on-failure
|
||||
ports:
|
||||
- '3000:3000'
|
||||
- '3001:3001'
|
||||
environment:
|
||||
- USERNAME=admin
|
||||
- PASSWORD=orange
|
||||
|
|
@ -111,6 +112,7 @@ services:
|
|||
restart: on-failure
|
||||
ports:
|
||||
- '3000:3000'
|
||||
- '3001:3001'
|
||||
environment:
|
||||
- USERNAME=admin
|
||||
- PASSWORD=orange
|
||||
|
|
@ -147,6 +149,7 @@ services:
|
|||
restart: on-failure
|
||||
ports:
|
||||
- '3000:3000'
|
||||
- '3001:3001'
|
||||
environment:
|
||||
- USERNAME=admin
|
||||
- PASSWORD=orange
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
// 主题初始化脚本 - 立即执行,避免主题闪烁
|
||||
(function () {
|
||||
try {
|
||||
// 应用主题函数
|
||||
function applyTheme(themeId, css) {
|
||||
const html = document.documentElement;
|
||||
|
||||
// 移除所有主题属性
|
||||
html.removeAttribute('data-theme');
|
||||
|
||||
// 应用主题
|
||||
if (themeId !== 'default') {
|
||||
html.setAttribute('data-theme', themeId);
|
||||
}
|
||||
|
||||
// 应用自定义CSS
|
||||
if (css) {
|
||||
let customStyleEl = document.getElementById('init-theme-css');
|
||||
if (!customStyleEl) {
|
||||
customStyleEl = document.createElement('style');
|
||||
customStyleEl.id = 'init-theme-css';
|
||||
document.head.appendChild(customStyleEl);
|
||||
}
|
||||
customStyleEl.textContent = css;
|
||||
}
|
||||
}
|
||||
|
||||
// 从localStorage获取保存的主题
|
||||
const savedTheme = localStorage.getItem('app-theme');
|
||||
const savedCustomCSS = localStorage.getItem('app-custom-css') || '';
|
||||
|
||||
// 立即应用已保存的主题(如果有)
|
||||
if (savedTheme) {
|
||||
applyTheme(savedTheme, savedCustomCSS);
|
||||
console.log('主题已初始化(本地设置):', savedTheme);
|
||||
} else {
|
||||
// 没有用户设置时,先应用默认主题
|
||||
applyTheme('default', '');
|
||||
console.log('主题已初始化(默认)');
|
||||
}
|
||||
|
||||
// 注意:GlobalThemeLoader会在React组件挂载后进一步处理全站配置
|
||||
} catch (error) {
|
||||
console.error('主题初始化失败:', error);
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
|
|
@ -20,41 +20,74 @@ export async function GET(request: NextRequest) {
|
|||
}
|
||||
|
||||
const authInfo = getAuthInfoFromCookie(request);
|
||||
if (!authInfo || !authInfo.username) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
const username = authInfo.username;
|
||||
const username = authInfo?.username;
|
||||
|
||||
try {
|
||||
const config = await getConfig();
|
||||
const result: AdminConfigResult = {
|
||||
Role: 'owner',
|
||||
Config: config,
|
||||
};
|
||||
|
||||
// 检查用户权限
|
||||
let userRole = 'guest'; // 未登录用户为 guest
|
||||
let isAdmin = false;
|
||||
|
||||
if (username === process.env.USERNAME) {
|
||||
result.Role = 'owner';
|
||||
} else {
|
||||
userRole = 'owner';
|
||||
isAdmin = true;
|
||||
} else if (username) {
|
||||
const user = config.UserConfig.Users.find((u) => u.username === username);
|
||||
if (user && user.role === 'admin' && !user.banned) {
|
||||
result.Role = 'admin';
|
||||
userRole = 'admin';
|
||||
isAdmin = true;
|
||||
} else if (user && !user.banned) {
|
||||
userRole = 'user';
|
||||
} else if (user && user.banned) {
|
||||
userRole = 'banned';
|
||||
} else {
|
||||
return NextResponse.json(
|
||||
{ error: '你是管理员吗你就访问?' },
|
||||
{ status: 401 }
|
||||
);
|
||||
// 认证了但用户不存在,可能是数据不同步
|
||||
userRole = 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
// 根据用户权限返回不同的配置信息
|
||||
if (isAdmin) {
|
||||
// 管理员返回完整配置
|
||||
const result: AdminConfigResult = {
|
||||
Role: userRole as 'admin' | 'owner',
|
||||
Config: config,
|
||||
};
|
||||
|
||||
return NextResponse.json(result, {
|
||||
headers: {
|
||||
'Cache-Control': 'no-store', // 管理员配置不缓存
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// 普通用户或未登录用户只返回公开配置
|
||||
const publicConfig = {
|
||||
ThemeConfig: config.ThemeConfig,
|
||||
SiteConfig: {
|
||||
SiteName: config.SiteConfig.SiteName,
|
||||
Announcement: config.SiteConfig.Announcement,
|
||||
// 其他公开的站点配置可以在这里添加
|
||||
}
|
||||
};
|
||||
|
||||
const result = {
|
||||
Role: userRole,
|
||||
Config: publicConfig,
|
||||
};
|
||||
|
||||
console.log('返回公开配置给', userRole, ',包含主题配置:', !!publicConfig.ThemeConfig);
|
||||
return NextResponse.json(result, {
|
||||
headers: {
|
||||
'Cache-Control': 'public, max-age=60', // 公开配置可以缓存1分钟
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取管理员配置失败:', error);
|
||||
console.error('获取配置失败:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: '获取管理员配置失败',
|
||||
error: '获取配置失败',
|
||||
details: (error as Error).message,
|
||||
},
|
||||
{ status: 500 }
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { getAuthInfoFromCookie } from '@/lib/auth';
|
|||
import { db } from '@/lib/db';
|
||||
import { AdminConfig } from '@/lib/admin.types';
|
||||
import { headers, cookies } from 'next/headers';
|
||||
import { getConfig, setCachedConfig, clearCachedConfig } from '@/lib/config';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
|
|
@ -22,12 +23,8 @@ export async function GET() {
|
|||
return NextResponse.json({ error: '认证信息无效' }, { status: 401 });
|
||||
}
|
||||
|
||||
const config = await db.getAdminConfig();
|
||||
const themeConfig = config?.ThemeConfig || {
|
||||
defaultTheme: 'default' as const,
|
||||
customCSS: '',
|
||||
allowUserCustomization: true,
|
||||
};
|
||||
const config = await getConfig();
|
||||
const themeConfig = config.ThemeConfig;
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
|
|
@ -75,40 +72,7 @@ export async function POST(request: Request) {
|
|||
}
|
||||
|
||||
// 获取当前配置
|
||||
const currentConfig = await db.getAdminConfig();
|
||||
|
||||
// 如果没有配置,创建一个基础配置
|
||||
let baseConfig: AdminConfig;
|
||||
if (!currentConfig) {
|
||||
baseConfig = {
|
||||
ConfigSubscribtion: {
|
||||
URL: "",
|
||||
AutoUpdate: false,
|
||||
LastCheck: "",
|
||||
},
|
||||
ConfigFile: "",
|
||||
SiteConfig: {
|
||||
SiteName: "OrangeTV",
|
||||
Announcement: "",
|
||||
SearchDownstreamMaxPage: 10,
|
||||
SiteInterfaceCacheTime: 30,
|
||||
DoubanProxyType: "direct",
|
||||
DoubanProxy: "",
|
||||
DoubanImageProxyType: "direct",
|
||||
DoubanImageProxy: "",
|
||||
DisableYellowFilter: false,
|
||||
FluidSearch: true,
|
||||
RequireDeviceCode: false,
|
||||
},
|
||||
UserConfig: {
|
||||
Users: [],
|
||||
},
|
||||
SourceConfig: [],
|
||||
CustomCategories: [],
|
||||
};
|
||||
} else {
|
||||
baseConfig = currentConfig;
|
||||
}
|
||||
const baseConfig = await getConfig();
|
||||
|
||||
// 更新主题配置
|
||||
const updatedConfig: AdminConfig = {
|
||||
|
|
@ -120,10 +84,23 @@ export async function POST(request: Request) {
|
|||
},
|
||||
};
|
||||
|
||||
console.log('保存主题配置:', updatedConfig.ThemeConfig);
|
||||
console.log('=== 保存主题配置 ===');
|
||||
console.log('请求参数:', { defaultTheme, customCSS, allowUserCustomization });
|
||||
console.log('当前存储类型:', process.env.NEXT_PUBLIC_STORAGE_TYPE || 'localstorage');
|
||||
console.log('待保存配置:', updatedConfig.ThemeConfig);
|
||||
console.log('完整配置对象:', JSON.stringify(updatedConfig, null, 2));
|
||||
|
||||
await db.saveAdminConfig(updatedConfig);
|
||||
console.log('主题配置保存成功');
|
||||
|
||||
// 直接更新缓存,确保缓存与数据库同步
|
||||
await setCachedConfig(updatedConfig);
|
||||
console.log('已更新配置缓存');
|
||||
|
||||
// 立即验证缓存中的配置
|
||||
const cachedConfig = await getConfig();
|
||||
console.log('保存后验证缓存中的配置:', cachedConfig.ThemeConfig);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '主题配置已更新',
|
||||
|
|
|
|||
|
|
@ -1,31 +0,0 @@
|
|||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/lib/db';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const config = await db.getAdminConfig();
|
||||
const themeConfig = config?.ThemeConfig || {
|
||||
defaultTheme: 'default' as const,
|
||||
customCSS: '',
|
||||
allowUserCustomization: true,
|
||||
};
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: themeConfig,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取主题配置失败:', error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
data: {
|
||||
defaultTheme: 'default' as const,
|
||||
customCSS: '',
|
||||
allowUserCustomization: true,
|
||||
}
|
||||
},
|
||||
{ status: 200 } // 返回默认配置而不是错误
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -298,11 +298,12 @@ function DoubanPageClient() {
|
|||
id: item.id?.toString() || '',
|
||||
title: item.name_cn || item.name,
|
||||
poster:
|
||||
item.images.large ||
|
||||
item.images.common ||
|
||||
item.images.medium ||
|
||||
item.images.small ||
|
||||
item.images.grid,
|
||||
item.images?.large ||
|
||||
item.images?.common ||
|
||||
item.images?.medium ||
|
||||
item.images?.small ||
|
||||
item.images?.grid ||
|
||||
'', // 空字符串,让 VideoCard 组件处理图片加载失败
|
||||
rate: item.rating?.score?.toFixed(1) || '',
|
||||
year: item.air_date?.split('-')?.[0] || '',
|
||||
})),
|
||||
|
|
|
|||
|
|
@ -112,8 +112,59 @@ export default async function RootLayout({
|
|||
}}
|
||||
/>
|
||||
|
||||
{/* 主题初始化脚本 - 立即执行避免主题闪烁 */}
|
||||
<script src="/theme-init.js" />
|
||||
{/* 立即从缓存应用主题,避免闪烁 */}
|
||||
{/* eslint-disable-next-line @next/next/no-sync-scripts */}
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function() {
|
||||
try {
|
||||
// 从localStorage立即获取缓存的主题配置
|
||||
const cachedTheme = localStorage.getItem('theme-cache');
|
||||
|
||||
if (cachedTheme) {
|
||||
try {
|
||||
const themeConfig = JSON.parse(cachedTheme);
|
||||
console.log('应用缓存主题配置:', themeConfig);
|
||||
|
||||
// 立即应用缓存的主题,避免闪烁
|
||||
const html = document.documentElement;
|
||||
|
||||
// 清除现有主题
|
||||
html.removeAttribute('data-theme');
|
||||
|
||||
// 应用缓存的主题
|
||||
if (themeConfig.defaultTheme && themeConfig.defaultTheme !== 'default') {
|
||||
html.setAttribute('data-theme', themeConfig.defaultTheme);
|
||||
}
|
||||
|
||||
// 应用缓存的自定义CSS
|
||||
if (themeConfig.customCSS) {
|
||||
let customStyleEl = document.getElementById('custom-theme-css');
|
||||
if (!customStyleEl) {
|
||||
customStyleEl = document.createElement('style');
|
||||
customStyleEl.id = 'custom-theme-css';
|
||||
document.head.appendChild(customStyleEl);
|
||||
}
|
||||
customStyleEl.textContent = themeConfig.customCSS;
|
||||
}
|
||||
|
||||
console.log('缓存主题已应用:', themeConfig.defaultTheme);
|
||||
} catch (parseError) {
|
||||
console.warn('解析缓存主题配置失败:', parseError);
|
||||
localStorage.removeItem('theme-cache'); // 清除无效缓存
|
||||
}
|
||||
} else {
|
||||
console.log('未找到缓存主题,等待API获取');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('应用缓存主题失败:', error);
|
||||
}
|
||||
})();
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
|
||||
</head>
|
||||
<body
|
||||
className={`${inter.className} min-h-screen bg-white text-gray-900 dark:bg-black dark:text-gray-200`}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ import { DoubanItem } from '@/lib/types';
|
|||
import CapsuleSwitch from '@/components/CapsuleSwitch';
|
||||
import ContinueWatching from '@/components/ContinueWatching';
|
||||
import PageLayout from '@/components/PageLayout';
|
||||
import { useThemeInit } from '@/hooks/useTheme';
|
||||
import ScrollableRow from '@/components/ScrollableRow';
|
||||
import { useSite } from '@/components/SiteProvider';
|
||||
import VideoCard from '@/components/VideoCard';
|
||||
|
|
@ -373,20 +372,21 @@ function HomeClient() {
|
|||
|
||||
return todayAnimes.map((anime, index) => (
|
||||
<div
|
||||
key={`${anime.id}-${index}`}
|
||||
key={`${anime.id || 0}-${index}`}
|
||||
className='min-w-[96px] w-24 sm:min-w-[180px] sm:w-44'
|
||||
>
|
||||
<VideoCard
|
||||
from='douban'
|
||||
title={anime.name_cn || anime.name}
|
||||
title={anime.name_cn || anime.name || '未知标题'}
|
||||
poster={
|
||||
anime.images.large ||
|
||||
anime.images.common ||
|
||||
anime.images.medium ||
|
||||
anime.images.small ||
|
||||
anime.images.grid
|
||||
anime.images?.large ||
|
||||
anime.images?.common ||
|
||||
anime.images?.medium ||
|
||||
anime.images?.small ||
|
||||
anime.images?.grid ||
|
||||
'' // 空字符串,让 VideoCard 组件处理图片加载失败
|
||||
}
|
||||
douban_id={anime.id}
|
||||
douban_id={anime.id || 0}
|
||||
rate={anime.rating?.score?.toFixed(1) || ''}
|
||||
year={anime.air_date?.split('-')?.[0] || ''}
|
||||
isBangumi={true}
|
||||
|
|
@ -516,8 +516,6 @@ function HomeClient() {
|
|||
}
|
||||
|
||||
export default function Home() {
|
||||
// 初始化主题
|
||||
useThemeInit();
|
||||
return (
|
||||
<Suspense>
|
||||
<HomeClient />
|
||||
|
|
|
|||
|
|
@ -2,71 +2,95 @@
|
|||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
// 全局主题加载器组件
|
||||
// 全局主题加载器组件 - 从API同步最新配置,确保缓存与服务端一致
|
||||
const GlobalThemeLoader = () => {
|
||||
useEffect(() => {
|
||||
const loadGlobalTheme = async () => {
|
||||
const syncThemeWithAPI = async () => {
|
||||
try {
|
||||
// 获取全局主题配置
|
||||
const response = await fetch('/api/theme');
|
||||
console.log('从API同步主题配置...');
|
||||
const response = await fetch('/api/admin/config');
|
||||
const result = await response.json();
|
||||
|
||||
console.log('获取到全站主题配置:', result);
|
||||
if (result?.Config?.ThemeConfig) {
|
||||
const themeConfig = result.Config.ThemeConfig;
|
||||
const { defaultTheme, customCSS, allowUserCustomization } = themeConfig;
|
||||
|
||||
if (result.success && result.data) {
|
||||
const { defaultTheme, customCSS, allowUserCustomization } = result.data;
|
||||
console.log('API返回主题配置:', {
|
||||
defaultTheme,
|
||||
customCSS,
|
||||
allowUserCustomization
|
||||
});
|
||||
|
||||
// 检查用户是否有自定义设置
|
||||
const userTheme = localStorage.getItem('app-theme');
|
||||
const userCustomCSS = localStorage.getItem('app-custom-css') || '';
|
||||
// 获取当前缓存的主题配置
|
||||
const cachedTheme = getCachedTheme();
|
||||
|
||||
console.log('当前用户设置:', { userTheme, userCustomCSS });
|
||||
console.log('全站配置:', { defaultTheme, customCSS, allowUserCustomization });
|
||||
// 比较API配置与缓存配置
|
||||
const configChanged = !cachedTheme ||
|
||||
cachedTheme.defaultTheme !== defaultTheme ||
|
||||
cachedTheme.customCSS !== customCSS;
|
||||
|
||||
// 如果不允许用户自定义,强制应用全局配置
|
||||
if (!allowUserCustomization) {
|
||||
localStorage.setItem('app-theme', defaultTheme);
|
||||
localStorage.setItem('app-custom-css', customCSS);
|
||||
applyTheme(defaultTheme, customCSS);
|
||||
console.log('强制应用全站配置:', defaultTheme);
|
||||
return;
|
||||
}
|
||||
|
||||
// 智能决定使用哪个主题
|
||||
let finalTheme = defaultTheme;
|
||||
let finalCSS = customCSS;
|
||||
|
||||
// 检查是否需要强制应用全站主题
|
||||
// 如果localStorage中存储的主题与全站默认不同,说明可能是过期的设置,需要更新
|
||||
const shouldForceGlobalTheme = !userTheme || userTheme !== defaultTheme;
|
||||
|
||||
if (shouldForceGlobalTheme) {
|
||||
// 强制应用全站默认配置
|
||||
finalTheme = defaultTheme;
|
||||
finalCSS = customCSS;
|
||||
localStorage.setItem('app-theme', defaultTheme);
|
||||
if (customCSS) {
|
||||
localStorage.setItem('app-custom-css', customCSS);
|
||||
if (configChanged) {
|
||||
console.log('检测到主题配置变更,更新应用:', {
|
||||
from: cachedTheme,
|
||||
to: { defaultTheme, customCSS }
|
||||
});
|
||||
applyAndCacheTheme(defaultTheme, customCSS);
|
||||
} else {
|
||||
localStorage.removeItem('app-custom-css');
|
||||
}
|
||||
console.log('强制应用全站默认主题:', defaultTheme, '(替换过期设置:', userTheme, ')');
|
||||
} else {
|
||||
// 用户设置与全站默认一致,使用现有设置
|
||||
finalTheme = userTheme;
|
||||
finalCSS = userCustomCSS || customCSS;
|
||||
console.log('保持一致的主题设置:', userTheme);
|
||||
console.log('主题配置无变化,保持当前设置');
|
||||
}
|
||||
|
||||
// 应用最终主题
|
||||
applyTheme(finalTheme, finalCSS);
|
||||
// 将配置存储到运行时配置中,供ThemeManager使用
|
||||
const runtimeConfig = (window as any).RUNTIME_CONFIG;
|
||||
if (runtimeConfig) {
|
||||
runtimeConfig.THEME_CONFIG = themeConfig;
|
||||
}
|
||||
} else {
|
||||
console.log('无法获取主题配置,使用默认配置:', result);
|
||||
// API失败时,如果有缓存就保持,没有缓存就用默认
|
||||
const cachedTheme = getCachedTheme();
|
||||
if (!cachedTheme) {
|
||||
console.log('无缓存,应用默认主题');
|
||||
applyAndCacheTheme('default', '');
|
||||
} else {
|
||||
console.log('保持缓存主题:', cachedTheme);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载全局主题配置失败:', error);
|
||||
// 失败时使用本地设置或默认设置
|
||||
const savedTheme = localStorage.getItem('app-theme') || 'default';
|
||||
const savedCustomCSS = localStorage.getItem('app-custom-css') || '';
|
||||
applyTheme(savedTheme, savedCustomCSS);
|
||||
console.error('API同步失败:', error);
|
||||
// 错误时如果有缓存就保持,没有缓存就用默认
|
||||
const cachedTheme = getCachedTheme();
|
||||
if (!cachedTheme) {
|
||||
console.log('无缓存且请求失败,应用默认主题');
|
||||
applyAndCacheTheme('default', '');
|
||||
} else {
|
||||
console.log('请求失败,保持缓存主题:', cachedTheme);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 获取缓存的主题配置
|
||||
const getCachedTheme = () => {
|
||||
try {
|
||||
const cached = localStorage.getItem('theme-cache');
|
||||
return cached ? JSON.parse(cached) : null;
|
||||
} catch (error) {
|
||||
console.warn('读取主题缓存失败:', error);
|
||||
localStorage.removeItem('theme-cache');
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// 应用主题并缓存
|
||||
const applyAndCacheTheme = (themeId: string, css: string = '') => {
|
||||
applyTheme(themeId, css);
|
||||
|
||||
// 缓存主题配置
|
||||
const themeConfig = { defaultTheme: themeId, customCSS: css };
|
||||
try {
|
||||
localStorage.setItem('theme-cache', JSON.stringify(themeConfig));
|
||||
console.log('主题配置已缓存:', themeConfig);
|
||||
} catch (error) {
|
||||
console.warn('缓存主题配置失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -83,17 +107,21 @@ const GlobalThemeLoader = () => {
|
|||
}
|
||||
|
||||
// 应用自定义CSS
|
||||
let customStyleEl = document.getElementById('global-theme-css');
|
||||
let customStyleEl = document.getElementById('custom-theme-css');
|
||||
if (!customStyleEl) {
|
||||
customStyleEl = document.createElement('style');
|
||||
customStyleEl.id = 'global-theme-css';
|
||||
customStyleEl.id = 'custom-theme-css';
|
||||
document.head.appendChild(customStyleEl);
|
||||
}
|
||||
customStyleEl.textContent = css;
|
||||
};
|
||||
|
||||
// 立即加载,不延迟
|
||||
loadGlobalTheme();
|
||||
// 延迟一点时间,确保页面缓存主题已应用,然后同步API配置
|
||||
const timer = setTimeout(() => {
|
||||
syncThemeWithAPI();
|
||||
}, 100);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
return null; // 这是一个逻辑组件,不渲染任何内容
|
||||
|
|
|
|||
|
|
@ -274,21 +274,47 @@ const ThemeManager = ({ showAlert, role }: ThemeManagerProps) => {
|
|||
customCSS: string;
|
||||
allowUserCustomization: boolean;
|
||||
} | null>(null);
|
||||
const [isGlobalMode, setIsGlobalMode] = useState(false);
|
||||
|
||||
const isAdmin = role === 'admin' || role === 'owner';
|
||||
|
||||
// 加载全局主题配置
|
||||
// 更新主题缓存的辅助函数
|
||||
const updateThemeCache = (themeId: string, css: string) => {
|
||||
try {
|
||||
const themeConfig = {
|
||||
defaultTheme: themeId,
|
||||
customCSS: css
|
||||
};
|
||||
localStorage.setItem('theme-cache', JSON.stringify(themeConfig));
|
||||
console.log('主题配置已缓存:', themeConfig);
|
||||
} catch (error) {
|
||||
console.warn('缓存主题配置失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 从API加载主题配置(唯一数据源)
|
||||
const loadGlobalThemeConfig = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/theme');
|
||||
console.log('从API获取主题配置...');
|
||||
const response = await fetch('/api/admin/config');
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
setGlobalThemeConfig(result.data);
|
||||
return result.data;
|
||||
|
||||
if (result?.Config?.ThemeConfig) {
|
||||
const themeConfig = result.Config.ThemeConfig;
|
||||
console.log('API返回的主题配置:', themeConfig);
|
||||
setGlobalThemeConfig(themeConfig);
|
||||
|
||||
// 更新运行时配置,保持同步
|
||||
const runtimeConfig = (window as any).RUNTIME_CONFIG;
|
||||
if (runtimeConfig) {
|
||||
runtimeConfig.THEME_CONFIG = themeConfig;
|
||||
}
|
||||
|
||||
return themeConfig;
|
||||
} else {
|
||||
console.log('无法获取主题配置,可能未登录或权限不足:', result);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载全局主题配置失败:', error);
|
||||
console.error('从API加载主题配置失败:', error);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
|
@ -308,9 +334,25 @@ const ThemeManager = ({ showAlert, role }: ThemeManagerProps) => {
|
|||
const result = await response.json();
|
||||
if (result.success) {
|
||||
setGlobalThemeConfig(result.data);
|
||||
|
||||
// 更新运行时配置,确保同步
|
||||
const runtimeConfig = (window as any).RUNTIME_CONFIG;
|
||||
if (runtimeConfig) {
|
||||
runtimeConfig.THEME_CONFIG = result.data;
|
||||
console.log('已更新运行时主题配置:', result.data);
|
||||
}
|
||||
|
||||
// 立即应用新的主题配置,确保当前页面也能看到更改
|
||||
applyTheme(result.data.defaultTheme, result.data.customCSS);
|
||||
|
||||
// 更新本地缓存
|
||||
updateThemeCache(result.data.defaultTheme, result.data.customCSS);
|
||||
|
||||
console.log('已立即应用新主题配置:', result.data.defaultTheme);
|
||||
|
||||
showAlert({
|
||||
type: 'success',
|
||||
title: '全局主题配置已保存',
|
||||
title: '全站主题配置已保存',
|
||||
message: '所有用户将使用新的主题配置',
|
||||
timer: 3000
|
||||
});
|
||||
|
|
@ -338,23 +380,23 @@ const ThemeManager = ({ showAlert, role }: ThemeManagerProps) => {
|
|||
// 加载全局配置
|
||||
const globalConfig = await loadGlobalThemeConfig();
|
||||
|
||||
if (isGlobalMode && globalConfig) {
|
||||
if (globalConfig) {
|
||||
// 使用全局配置
|
||||
setCurrentTheme(globalConfig.defaultTheme);
|
||||
setCustomCSS(globalConfig.customCSS);
|
||||
applyTheme(globalConfig.defaultTheme, globalConfig.customCSS);
|
||||
} else {
|
||||
// 使用本地配置
|
||||
const savedTheme = localStorage.getItem('app-theme') || globalConfig?.defaultTheme || 'default';
|
||||
const savedCustomCSS = localStorage.getItem('app-custom-css') || '';
|
||||
setCurrentTheme(savedTheme);
|
||||
setCustomCSS(savedCustomCSS);
|
||||
applyTheme(savedTheme, savedCustomCSS);
|
||||
// 如果没有全局配置,使用默认值
|
||||
const defaultTheme = 'default';
|
||||
const defaultCSS = '';
|
||||
setCurrentTheme(defaultTheme);
|
||||
setCustomCSS(defaultCSS);
|
||||
applyTheme(defaultTheme, defaultCSS);
|
||||
}
|
||||
};
|
||||
|
||||
initTheme();
|
||||
}, [isGlobalMode]);
|
||||
}, []);
|
||||
|
||||
// 应用主题
|
||||
const applyTheme = (themeId: string, css: string = '') => {
|
||||
|
|
@ -383,7 +425,7 @@ const ThemeManager = ({ showAlert, role }: ThemeManagerProps) => {
|
|||
setCurrentTheme(themeId);
|
||||
applyTheme(themeId, customCSS);
|
||||
|
||||
if (isGlobalMode && isAdmin) {
|
||||
if (isAdmin) {
|
||||
// 保存到全局配置
|
||||
const success = await saveGlobalThemeConfig({
|
||||
defaultTheme: themeId,
|
||||
|
|
@ -399,15 +441,12 @@ const ThemeManager = ({ showAlert, role }: ThemeManagerProps) => {
|
|||
allowUserCustomization: globalThemeConfig?.allowUserCustomization ?? true,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 保存到本地
|
||||
localStorage.setItem('app-theme', themeId);
|
||||
}
|
||||
|
||||
const theme = themes.find(t => t.id === themeId);
|
||||
showAlert({
|
||||
type: 'success',
|
||||
title: isGlobalMode ? '全局主题已设置' : '主题已切换',
|
||||
title: '全站主题已设置',
|
||||
message: `已切换到${theme?.name}`,
|
||||
timer: 2000
|
||||
});
|
||||
|
|
@ -432,7 +471,7 @@ const ThemeManager = ({ showAlert, role }: ThemeManagerProps) => {
|
|||
try {
|
||||
applyTheme(currentTheme, customCSS);
|
||||
|
||||
if (isGlobalMode && isAdmin) {
|
||||
if (isAdmin) {
|
||||
// 保存到全局配置
|
||||
const success = await saveGlobalThemeConfig({
|
||||
defaultTheme: currentTheme,
|
||||
|
|
@ -449,12 +488,10 @@ const ThemeManager = ({ showAlert, role }: ThemeManagerProps) => {
|
|||
});
|
||||
}
|
||||
} else {
|
||||
// 保存到本地
|
||||
localStorage.setItem('app-custom-css', customCSS);
|
||||
showAlert({
|
||||
type: 'success',
|
||||
title: '自定义样式已应用',
|
||||
message: '您的自定义CSS已生效',
|
||||
type: 'warning',
|
||||
title: '权限不足',
|
||||
message: '仅管理员可以设置全站主题',
|
||||
timer: 2000
|
||||
});
|
||||
}
|
||||
|
|
@ -469,14 +506,41 @@ const ThemeManager = ({ showAlert, role }: ThemeManagerProps) => {
|
|||
};
|
||||
|
||||
// 重置自定义CSS
|
||||
const handleCustomCSSReset = () => {
|
||||
const handleCustomCSSReset = async () => {
|
||||
setCustomCSS('');
|
||||
applyTheme(currentTheme, '');
|
||||
localStorage.removeItem('app-custom-css');
|
||||
|
||||
if (isAdmin) {
|
||||
// 保存到全局配置
|
||||
await saveGlobalThemeConfig({
|
||||
defaultTheme: currentTheme,
|
||||
customCSS: '',
|
||||
allowUserCustomization: globalThemeConfig?.allowUserCustomization ?? true,
|
||||
});
|
||||
|
||||
setGlobalThemeConfig({
|
||||
defaultTheme: currentTheme,
|
||||
customCSS: '',
|
||||
allowUserCustomization: globalThemeConfig?.allowUserCustomization ?? true,
|
||||
});
|
||||
|
||||
// 更新运行时配置
|
||||
const runtimeConfig = (window as any).RUNTIME_CONFIG;
|
||||
if (runtimeConfig) {
|
||||
runtimeConfig.THEME_CONFIG = {
|
||||
defaultTheme: currentTheme,
|
||||
customCSS: '',
|
||||
allowUserCustomization: globalThemeConfig?.allowUserCustomization ?? true,
|
||||
};
|
||||
}
|
||||
|
||||
// 更新本地缓存
|
||||
updateThemeCache(currentTheme, '');
|
||||
}
|
||||
|
||||
showAlert({
|
||||
type: 'success',
|
||||
title: '自定义样式已重置',
|
||||
title: '全站自定义样式已重置',
|
||||
timer: 2000
|
||||
});
|
||||
};
|
||||
|
|
@ -495,46 +559,14 @@ const ThemeManager = ({ showAlert, role }: ThemeManagerProps) => {
|
|||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 管理员控制面板 */}
|
||||
{isAdmin && (
|
||||
{isAdmin && globalThemeConfig && (
|
||||
<div className="bg-theme-surface border border-theme-border rounded-lg p-4">
|
||||
<h3 className="text-lg font-semibold text-theme-text mb-4 flex items-center gap-2">
|
||||
<Palette className="h-5 w-5" />
|
||||
管理员主题设置
|
||||
全站主题设置
|
||||
</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-theme-text">配置模式</label>
|
||||
<p className="text-xs text-theme-text-secondary mt-1">
|
||||
选择设置个人主题还是全站默认主题
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name="configMode"
|
||||
checked={!isGlobalMode}
|
||||
onChange={() => setIsGlobalMode(false)}
|
||||
className="w-4 h-4 text-theme-accent"
|
||||
/>
|
||||
<span className="text-sm text-theme-text">个人设置</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name="configMode"
|
||||
checked={isGlobalMode}
|
||||
onChange={() => setIsGlobalMode(true)}
|
||||
className="w-4 h-4 text-theme-accent"
|
||||
/>
|
||||
<span className="text-sm text-theme-text">全站默认</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{globalThemeConfig && (
|
||||
<div className="p-3 bg-theme-accent/5 border border-theme-accent/20 rounded-lg">
|
||||
<div className="text-sm text-theme-text">
|
||||
<strong>当前全站配置:</strong>
|
||||
|
|
@ -545,18 +577,15 @@ const ThemeManager = ({ showAlert, role }: ThemeManagerProps) => {
|
|||
{!globalThemeConfig.allowUserCustomization && ' | 禁止用户自定义'}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isGlobalMode && (
|
||||
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded-lg dark:bg-yellow-900/20 dark:border-yellow-700">
|
||||
<div className="flex items-center gap-2 text-yellow-800 dark:text-yellow-200">
|
||||
<span className="text-sm font-medium">⚠️ 全站模式</span>
|
||||
<div className="p-3 bg-blue-50 border border-blue-200 rounded-lg dark:bg-blue-900/20 dark:border-blue-700">
|
||||
<div className="flex items-center gap-2 text-blue-800 dark:text-blue-200">
|
||||
<span className="text-sm font-medium">ℹ️ 全站主题</span>
|
||||
</div>
|
||||
<p className="text-xs text-yellow-700 dark:text-yellow-300 mt-1">
|
||||
在此模式下的更改将影响所有用户的默认主题配置
|
||||
<p className="text-xs text-blue-700 dark:text-blue-300 mt-1">
|
||||
在此设置的主题配置将应用到整个网站,影响所有用户的默认体验
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -565,18 +594,18 @@ const ThemeManager = ({ showAlert, role }: ThemeManagerProps) => {
|
|||
<div>
|
||||
<h3 className="text-lg font-semibold text-theme-text mb-4 flex items-center gap-2">
|
||||
<Palette className="h-5 w-5" />
|
||||
{isGlobalMode && isAdmin ? '全站默认主题' : '主题选择'}
|
||||
全站主题选择
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{themes.map((theme) => (
|
||||
<div
|
||||
key={theme.id}
|
||||
className={`relative p-4 border-2 rounded-xl cursor-pointer transition-all ${currentTheme === theme.id
|
||||
className={`relative p-4 border-2 rounded-xl transition-all ${currentTheme === theme.id
|
||||
? 'border-theme-accent bg-theme-accent/5'
|
||||
: 'border-theme-border bg-theme-surface hover:border-theme-accent/50'
|
||||
}`}
|
||||
onClick={() => handleThemeChange(theme.id)}
|
||||
: 'border-theme-border bg-theme-surface'
|
||||
} ${isAdmin ? 'cursor-pointer hover:border-theme-accent/50' : 'cursor-not-allowed opacity-60'}`}
|
||||
onClick={() => isAdmin && handleThemeChange(theme.id)}
|
||||
>
|
||||
{/* 主题预览 */}
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
|
|
@ -589,11 +618,11 @@ const ThemeManager = ({ showAlert, role }: ThemeManagerProps) => {
|
|||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleThemePreview(theme.id);
|
||||
if (isAdmin) handleThemePreview(theme.id);
|
||||
}}
|
||||
className="p-1 text-theme-text-secondary hover:text-theme-accent transition-colors"
|
||||
title="预览主题"
|
||||
disabled={previewMode}
|
||||
className={`p-1 transition-colors ${isAdmin ? 'text-theme-text-secondary hover:text-theme-accent' : 'text-theme-text-secondary opacity-50 cursor-not-allowed'}`}
|
||||
title={isAdmin ? "预览主题" : "仅管理员可预览"}
|
||||
disabled={previewMode || !isAdmin}
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</button>
|
||||
|
|
@ -621,8 +650,9 @@ const ThemeManager = ({ showAlert, role }: ThemeManagerProps) => {
|
|||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-theme-text flex items-center gap-2">
|
||||
<Palette className="h-5 w-5" />
|
||||
自定义样式
|
||||
全站自定义样式
|
||||
</h3>
|
||||
{isAdmin ? (
|
||||
<button
|
||||
onClick={() => setShowCustomEditor(!showCustomEditor)}
|
||||
className="flex items-center gap-2 px-3 py-1.5 text-sm bg-theme-surface border border-theme-border rounded-lg hover:bg-theme-accent/5 transition-colors"
|
||||
|
|
@ -630,9 +660,25 @@ const ThemeManager = ({ showAlert, role }: ThemeManagerProps) => {
|
|||
{showCustomEditor ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||
{showCustomEditor ? '收起编辑器' : '展开编辑器'}
|
||||
</button>
|
||||
) : (
|
||||
<div className="text-sm text-theme-text-secondary">
|
||||
仅管理员可编辑
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showCustomEditor && (
|
||||
{!isAdmin && (
|
||||
<div className="p-4 bg-yellow-50 border border-yellow-200 rounded-lg dark:bg-yellow-900/20 dark:border-yellow-700 mb-4">
|
||||
<div className="flex items-center gap-2 text-yellow-800 dark:text-yellow-200">
|
||||
<span className="text-sm font-medium">⚠️ 权限限制</span>
|
||||
</div>
|
||||
<p className="text-xs text-yellow-700 dark:text-yellow-300 mt-1">
|
||||
您当前没有权限修改全站主题设置,请联系管理员。
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isAdmin && showCustomEditor && (
|
||||
<div className="space-y-4">
|
||||
<div className="text-sm text-theme-text-secondary bg-theme-surface p-3 rounded-lg border border-theme-border">
|
||||
<p className="mb-2">💡 <strong>使用提示:</strong></p>
|
||||
|
|
@ -685,12 +731,13 @@ const ThemeManager = ({ showAlert, role }: ThemeManagerProps) => {
|
|||
</div>
|
||||
|
||||
{/* CSS 模板库 */}
|
||||
{isAdmin && (
|
||||
<div className="bg-theme-surface border border-theme-border rounded-lg p-4">
|
||||
<h4 className="font-medium text-theme-text mb-3 flex items-center gap-2">
|
||||
<Palette className="h-4 w-4" />
|
||||
🎨 样式模板库
|
||||
🎨 全站样式模板库
|
||||
</h4>
|
||||
<p className="text-sm text-theme-text-secondary mb-4">选择预设模板快速应用炫酷效果,也可以在此基础上进行自定义修改</p>
|
||||
<p className="text-sm text-theme-text-secondary mb-4">选择预设模板快速应用炫酷效果到全站,也可以在此基础上进行自定义修改</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{cssTemplates.map((template) => (
|
||||
|
|
@ -718,14 +765,15 @@ const ThemeManager = ({ showAlert, role }: ThemeManagerProps) => {
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 使用说明 */}
|
||||
<div className="bg-theme-surface border border-theme-border rounded-lg p-4">
|
||||
<h4 className="font-medium text-theme-text mb-2">📖 主题定制指南</h4>
|
||||
<h4 className="font-medium text-theme-text mb-2">📖 全站主题定制指南</h4>
|
||||
<div className="text-sm text-theme-text-secondary space-y-2">
|
||||
<p><strong>内置主题:</strong>选择预设主题即可一键切换整体风格</p>
|
||||
<p><strong>自定义CSS:</strong>通过CSS变量或直接样式实现个性化定制</p>
|
||||
<p><strong>样式模板:</strong>使用预设模板快速实现炫酷效果</p>
|
||||
<p><strong>内置主题:</strong>{isAdmin ? '选择预设主题即可一键切换全站整体风格' : '由管理员设置的全站预设主题'}</p>
|
||||
{isAdmin && <p><strong>自定义CSS:</strong>通过CSS变量或直接样式实现全站个性化定制</p>}
|
||||
{isAdmin && <p><strong>样式模板:</strong>使用预设模板快速实现炫酷效果</p>}
|
||||
<p><strong>主题变量:</strong></p>
|
||||
<ul className="text-xs space-y-1 ml-4 mt-1">
|
||||
<li>• <code className="bg-theme-bg px-1 rounded">--color-theme-bg</code> - 背景色</li>
|
||||
|
|
@ -734,12 +782,16 @@ const ThemeManager = ({ showAlert, role }: ThemeManagerProps) => {
|
|||
<li>• <code className="bg-theme-bg px-1 rounded">--color-theme-text</code> - 主文本色</li>
|
||||
<li>• <code className="bg-theme-bg px-1 rounded">--color-theme-border</code> - 边框色</li>
|
||||
</ul>
|
||||
{isAdmin && (
|
||||
<>
|
||||
<p><strong>常用技巧:</strong></p>
|
||||
<ul className="text-xs space-y-1 ml-4 mt-1">
|
||||
<li>• 修改背景:<code className="bg-theme-bg px-1 rounded">{`body { background: linear-gradient(...); }`}</code></li>
|
||||
<li>• 使用Tailwind:<code className="bg-theme-bg px-1 rounded">{`.my-class { @apply bg-red-500; }`}</code></li>
|
||||
<li>• 组合多个模板效果获得独特样式</li>
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -88,9 +88,17 @@ export const VersionPanel: React.FC<VersionPanelProps> = ({
|
|||
const parsed = parseChangelog(content);
|
||||
setRemoteChangelog(parsed);
|
||||
|
||||
// 检查是否有更新
|
||||
// 检查是否有更新 - 基于日期而非版本号数字大小来确定最新版本
|
||||
if (parsed.length > 0) {
|
||||
const latest = parsed[0];
|
||||
// 按日期排序,找到真正的最新版本
|
||||
const sortedByDate = [...parsed].sort((a, b) => {
|
||||
// 解析日期进行比较
|
||||
const dateA = new Date(a.date);
|
||||
const dateB = new Date(b.date);
|
||||
return dateB.getTime() - dateA.getTime(); // 降序排列,最新的在前
|
||||
});
|
||||
|
||||
const latest = sortedByDate[0];
|
||||
setLatestVersion(latest.version);
|
||||
setIsHasUpdate(
|
||||
compareVersions(latest.version) === UpdateStatus.HAS_UPDATE
|
||||
|
|
@ -441,6 +449,12 @@ export const VersionPanel: React.FC<VersionPanelProps> = ({
|
|||
);
|
||||
return !localVersions.includes(entry.version);
|
||||
})
|
||||
.sort((a, b) => {
|
||||
// 按日期排序,确保最新的版本在前面显示
|
||||
const dateA = new Date(a.date);
|
||||
const dateB = new Date(b.date);
|
||||
return dateB.getTime() - dateA.getTime(); // 降序排列,最新的在前
|
||||
})
|
||||
.map((entry, index) => (
|
||||
<div
|
||||
key={index}
|
||||
|
|
|
|||
|
|
@ -1,87 +1,19 @@
|
|||
// 全局主题Hook - 用于在任何组件中初始化和使用主题
|
||||
import { useEffect } from 'react';
|
||||
// 全局主题Hook - 已弃用,主题现在由 GlobalThemeLoader 统一管理
|
||||
// 保留此文件是为了向后兼容性,但不再使用
|
||||
|
||||
export const useThemeInit = () => {
|
||||
useEffect(() => {
|
||||
// 确保在客户端环境中执行
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
try {
|
||||
// 从localStorage获取保存的主题
|
||||
const savedTheme = localStorage.getItem('app-theme') || 'default';
|
||||
const savedCustomCSS = localStorage.getItem('app-custom-css') || '';
|
||||
|
||||
// 立即应用主题到HTML元素
|
||||
const html = document.documentElement;
|
||||
|
||||
// 移除所有主题属性
|
||||
html.removeAttribute('data-theme');
|
||||
|
||||
// 应用保存的主题
|
||||
if (savedTheme !== 'default') {
|
||||
html.setAttribute('data-theme', savedTheme);
|
||||
}
|
||||
|
||||
// 应用自定义CSS
|
||||
if (savedCustomCSS) {
|
||||
let customStyleEl = document.getElementById('custom-theme-css');
|
||||
if (!customStyleEl) {
|
||||
customStyleEl = document.createElement('style');
|
||||
customStyleEl.id = 'custom-theme-css';
|
||||
document.head.appendChild(customStyleEl);
|
||||
}
|
||||
customStyleEl.textContent = savedCustomCSS;
|
||||
}
|
||||
|
||||
console.log(`主题已初始化: ${savedTheme}`);
|
||||
} catch (error) {
|
||||
console.error('主题初始化失败:', error);
|
||||
}
|
||||
}, []);
|
||||
// 不再执行任何操作,主题由 GlobalThemeLoader 处理
|
||||
console.warn('useThemeInit is deprecated. Theme is now managed by GlobalThemeLoader.');
|
||||
};
|
||||
|
||||
export const useTheme = () => {
|
||||
const applyTheme = (themeId: string, css: string = '') => {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const html = document.documentElement;
|
||||
|
||||
// 移除所有主题class
|
||||
html.removeAttribute('data-theme');
|
||||
|
||||
// 应用新主题
|
||||
if (themeId !== 'default') {
|
||||
html.setAttribute('data-theme', themeId);
|
||||
}
|
||||
|
||||
// 应用自定义CSS
|
||||
let customStyleEl = document.getElementById('custom-theme-css');
|
||||
if (!customStyleEl) {
|
||||
customStyleEl = document.createElement('style');
|
||||
customStyleEl.id = 'custom-theme-css';
|
||||
document.head.appendChild(customStyleEl);
|
||||
}
|
||||
customStyleEl.textContent = css;
|
||||
|
||||
// 保存到localStorage
|
||||
localStorage.setItem('app-theme', themeId);
|
||||
localStorage.setItem('app-custom-css', css);
|
||||
};
|
||||
|
||||
const getCurrentTheme = () => {
|
||||
if (typeof window === 'undefined') return 'default';
|
||||
return localStorage.getItem('app-theme') || 'default';
|
||||
};
|
||||
|
||||
const getCurrentCustomCSS = () => {
|
||||
if (typeof window === 'undefined') return '';
|
||||
return localStorage.getItem('app-custom-css') || '';
|
||||
};
|
||||
// 已弃用:主题现在由 GlobalThemeLoader 和 ThemeManager 统一管理
|
||||
console.warn('useTheme is deprecated. Use ThemeManager component for theme management.');
|
||||
|
||||
return {
|
||||
applyTheme,
|
||||
getCurrentTheme,
|
||||
getCurrentCustomCSS
|
||||
applyTheme: () => console.warn('applyTheme is deprecated'),
|
||||
getCurrentTheme: () => 'default',
|
||||
getCurrentCustomCSS: () => ''
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -379,6 +379,15 @@ export function configSelfCheck(adminConfig: AdminConfig): AdminConfig {
|
|||
adminConfig.SiteConfig.RequireDeviceCode = process.env.NEXT_PUBLIC_REQUIRE_DEVICE_CODE !== 'false';
|
||||
}
|
||||
|
||||
// 确保 ThemeConfig 存在
|
||||
if (!adminConfig.ThemeConfig) {
|
||||
adminConfig.ThemeConfig = {
|
||||
defaultTheme: 'default',
|
||||
customCSS: '',
|
||||
allowUserCustomization: true,
|
||||
};
|
||||
}
|
||||
|
||||
// 站长变更自检
|
||||
const ownerUser = process.env.USERNAME;
|
||||
|
||||
|
|
@ -517,3 +526,7 @@ export async function getAvailableApiSites(user?: string): Promise<ApiSite[]> {
|
|||
export async function setCachedConfig(config: AdminConfig) {
|
||||
cachedConfig = config;
|
||||
}
|
||||
|
||||
export function clearCachedConfig() {
|
||||
cachedConfig = undefined as any;
|
||||
}
|
||||
|
|
@ -72,7 +72,14 @@ export class UpstashRedisStorage implements IStorage {
|
|||
const val = await withRetry(() =>
|
||||
this.client.get(this.prKey(userName, key))
|
||||
);
|
||||
return val ? (val as PlayRecord) : null;
|
||||
if (!val) return null;
|
||||
|
||||
try {
|
||||
return typeof val === 'string' ? JSON.parse(val) : val as PlayRecord;
|
||||
} catch (error) {
|
||||
console.error('解析播放记录失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async setPlayRecord(
|
||||
|
|
@ -80,7 +87,7 @@ export class UpstashRedisStorage implements IStorage {
|
|||
key: string,
|
||||
record: PlayRecord
|
||||
): Promise<void> {
|
||||
await withRetry(() => this.client.set(this.prKey(userName, key), record));
|
||||
await withRetry(() => this.client.set(this.prKey(userName, key), JSON.stringify(record)));
|
||||
}
|
||||
|
||||
async getAllPlayRecords(
|
||||
|
|
@ -94,9 +101,14 @@ export class UpstashRedisStorage implements IStorage {
|
|||
for (const fullKey of keys) {
|
||||
const value = await withRetry(() => this.client.get(fullKey));
|
||||
if (value) {
|
||||
try {
|
||||
// 截取 source+id 部分
|
||||
const keyPart = ensureString(fullKey.replace(`u:${userName}:pr:`, ''));
|
||||
result[keyPart] = value as PlayRecord;
|
||||
const record = typeof value === 'string' ? JSON.parse(value) : value as PlayRecord;
|
||||
result[keyPart] = record;
|
||||
} catch (error) {
|
||||
console.error('解析播放记录失败:', error, 'key:', fullKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
|
@ -115,7 +127,14 @@ export class UpstashRedisStorage implements IStorage {
|
|||
const val = await withRetry(() =>
|
||||
this.client.get(this.favKey(userName, key))
|
||||
);
|
||||
return val ? (val as Favorite) : null;
|
||||
if (!val) return null;
|
||||
|
||||
try {
|
||||
return typeof val === 'string' ? JSON.parse(val) : val as Favorite;
|
||||
} catch (error) {
|
||||
console.error('解析收藏失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async setFavorite(
|
||||
|
|
@ -124,7 +143,7 @@ export class UpstashRedisStorage implements IStorage {
|
|||
favorite: Favorite
|
||||
): Promise<void> {
|
||||
await withRetry(() =>
|
||||
this.client.set(this.favKey(userName, key), favorite)
|
||||
this.client.set(this.favKey(userName, key), JSON.stringify(favorite))
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -137,8 +156,13 @@ export class UpstashRedisStorage implements IStorage {
|
|||
for (const fullKey of keys) {
|
||||
const value = await withRetry(() => this.client.get(fullKey));
|
||||
if (value) {
|
||||
try {
|
||||
const keyPart = ensureString(fullKey.replace(`u:${userName}:fav:`, ''));
|
||||
result[keyPart] = value as Favorite;
|
||||
const favorite = typeof value === 'string' ? JSON.parse(value) : value as Favorite;
|
||||
result[keyPart] = favorite;
|
||||
} catch (error) {
|
||||
console.error('解析收藏失败:', error, 'key:', fullKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
|
@ -270,11 +294,24 @@ export class UpstashRedisStorage implements IStorage {
|
|||
|
||||
async getAdminConfig(): Promise<AdminConfig | null> {
|
||||
const val = await withRetry(() => this.client.get(this.adminConfigKey()));
|
||||
return val ? (val as AdminConfig) : null;
|
||||
if (!val) return null;
|
||||
|
||||
try {
|
||||
// 尝试解析JSON字符串(兼容BaseRedisStorage格式)
|
||||
if (typeof val === 'string') {
|
||||
return JSON.parse(val) as AdminConfig;
|
||||
}
|
||||
// 如果已经是对象,直接返回(Upstash自动反序列化的情况)
|
||||
return val as AdminConfig;
|
||||
} catch (error) {
|
||||
console.error('解析管理员配置失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async setAdminConfig(config: AdminConfig): Promise<void> {
|
||||
await withRetry(() => this.client.set(this.adminConfigKey(), config));
|
||||
// 统一使用JSON字符串格式存储,与BaseRedisStorage保持一致
|
||||
await withRetry(() => this.client.set(this.adminConfigKey(), JSON.stringify(config)));
|
||||
}
|
||||
|
||||
// ---------- 跳过片头片尾配置 ----------
|
||||
|
|
@ -290,7 +327,14 @@ export class UpstashRedisStorage implements IStorage {
|
|||
const val = await withRetry(() =>
|
||||
this.client.get(this.skipConfigKey(userName, source, id))
|
||||
);
|
||||
return val ? (val as SkipConfig) : null;
|
||||
if (!val) return null;
|
||||
|
||||
try {
|
||||
return typeof val === 'string' ? JSON.parse(val) : val as SkipConfig;
|
||||
} catch (error) {
|
||||
console.error('解析跳过配置失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async setSkipConfig(
|
||||
|
|
@ -300,7 +344,7 @@ export class UpstashRedisStorage implements IStorage {
|
|||
config: SkipConfig
|
||||
): Promise<void> {
|
||||
await withRetry(() =>
|
||||
this.client.set(this.skipConfigKey(userName, source, id), config)
|
||||
this.client.set(this.skipConfigKey(userName, source, id), JSON.stringify(config))
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -332,11 +376,16 @@ export class UpstashRedisStorage implements IStorage {
|
|||
keys.forEach((key, index) => {
|
||||
const value = values[index];
|
||||
if (value) {
|
||||
try {
|
||||
// 从key中提取source+id
|
||||
const match = key.match(/^u:.+?:skip:(.+)$/);
|
||||
if (match) {
|
||||
const sourceAndId = match[1];
|
||||
configs[sourceAndId] = value as SkipConfig;
|
||||
const config = typeof value === 'string' ? JSON.parse(value) : value as SkipConfig;
|
||||
configs[sourceAndId] = config;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('解析跳过配置失败:', error, 'key:', key);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -373,7 +422,16 @@ export class UpstashRedisStorage implements IStorage {
|
|||
|
||||
async getDanmu(videoId: string): Promise<any[]> {
|
||||
const val = await withRetry(() => this.client.lrange(this.danmuKey(videoId), 0, -1));
|
||||
return val ? val.map(item => JSON.parse(ensureString(item))) : [];
|
||||
if (!val || !Array.isArray(val)) return [];
|
||||
|
||||
return val.map(item => {
|
||||
try {
|
||||
return typeof item === 'string' ? JSON.parse(item) : item;
|
||||
} catch (error) {
|
||||
console.error('解析弹幕数据失败:', error);
|
||||
return null;
|
||||
}
|
||||
}).filter(item => item !== null);
|
||||
}
|
||||
|
||||
async saveDanmu(videoId: string, userName: string, danmu: {
|
||||
|
|
@ -425,9 +483,11 @@ export class UpstashRedisStorage implements IStorage {
|
|||
if (!val) return null;
|
||||
|
||||
try {
|
||||
const data = JSON.parse(ensureString(val));
|
||||
// 处理不同的序列化格式
|
||||
const data = typeof val === 'string' ? JSON.parse(val) : val;
|
||||
return data.machineCode || null;
|
||||
} catch {
|
||||
} catch (error) {
|
||||
console.error('解析用户机器码失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -439,7 +499,7 @@ export class UpstashRedisStorage implements IStorage {
|
|||
bindTime: Date.now()
|
||||
};
|
||||
|
||||
// 保存用户的机器码
|
||||
// 保存用户的机器码 - 统一使用JSON序列化
|
||||
await withRetry(() =>
|
||||
this.client.set(this.machineCodeKey(userName), JSON.stringify(data))
|
||||
);
|
||||
|
|
@ -481,10 +541,11 @@ export class UpstashRedisStorage implements IStorage {
|
|||
|
||||
if (val) {
|
||||
try {
|
||||
const data = JSON.parse(ensureString(val));
|
||||
// 处理不同的序列化格式
|
||||
const data = typeof val === 'string' ? JSON.parse(val) : val;
|
||||
result[userName] = data;
|
||||
} catch {
|
||||
// 忽略解析错误
|
||||
} catch (error) {
|
||||
console.error('解析机器码用户数据失败:', error, 'key:', key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -536,9 +597,9 @@ export class UpstashRedisStorage implements IStorage {
|
|||
|
||||
// 消息管理
|
||||
async saveMessage(message: ChatMessage): Promise<void> {
|
||||
// 保存消息详情
|
||||
// 保存消息详情 - 使用JSON序列化
|
||||
await withRetry(() =>
|
||||
this.client.set(this.messageKey(message.id), message)
|
||||
this.client.set(this.messageKey(message.id), JSON.stringify(message))
|
||||
);
|
||||
|
||||
// 将消息ID添加到对话的消息列表中(按时间排序)
|
||||
|
|
@ -560,7 +621,12 @@ export class UpstashRedisStorage implements IStorage {
|
|||
for (const messageId of messageIds) {
|
||||
const messageData = await withRetry(() => this.client.get(this.messageKey(messageId as string)));
|
||||
if (messageData) {
|
||||
messages.push(messageData as ChatMessage);
|
||||
try {
|
||||
const message = typeof messageData === 'string' ? JSON.parse(messageData) : messageData;
|
||||
messages.push(message as ChatMessage);
|
||||
} catch (error) {
|
||||
console.error('解析消息失败:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -570,11 +636,15 @@ export class UpstashRedisStorage implements IStorage {
|
|||
async markMessageAsRead(messageId: string): Promise<void> {
|
||||
const messageData = await withRetry(() => this.client.get(this.messageKey(messageId)));
|
||||
if (messageData) {
|
||||
const message = messageData as ChatMessage;
|
||||
try {
|
||||
const message = typeof messageData === 'string' ? JSON.parse(messageData) : messageData as ChatMessage;
|
||||
message.is_read = true;
|
||||
await withRetry(() =>
|
||||
this.client.set(this.messageKey(messageId), message)
|
||||
this.client.set(this.messageKey(messageId), JSON.stringify(message))
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('标记消息为已读失败:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -601,13 +671,20 @@ export class UpstashRedisStorage implements IStorage {
|
|||
this.client.get(this.conversationKey(conversationId))
|
||||
);
|
||||
|
||||
return conversationData ? (conversationData as Conversation) : null;
|
||||
if (!conversationData) return null;
|
||||
|
||||
try {
|
||||
return typeof conversationData === 'string' ? JSON.parse(conversationData) : conversationData as Conversation;
|
||||
} catch (error) {
|
||||
console.error('解析对话失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async createConversation(conversation: Conversation): Promise<void> {
|
||||
// 保存对话详情
|
||||
// 保存对话详情 - 使用JSON序列化
|
||||
await withRetry(() =>
|
||||
this.client.set(this.conversationKey(conversation.id), conversation)
|
||||
this.client.set(this.conversationKey(conversation.id), JSON.stringify(conversation))
|
||||
);
|
||||
|
||||
// 将对话ID添加到每个参与者的对话列表中
|
||||
|
|
@ -623,7 +700,7 @@ export class UpstashRedisStorage implements IStorage {
|
|||
if (conversation) {
|
||||
Object.assign(conversation, updates);
|
||||
await withRetry(() =>
|
||||
this.client.set(this.conversationKey(conversationId), conversation)
|
||||
this.client.set(this.conversationKey(conversationId), JSON.stringify(conversation))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -656,7 +733,12 @@ export class UpstashRedisStorage implements IStorage {
|
|||
for (const friendId of friendIds) {
|
||||
const friendData = await withRetry(() => this.client.get(this.friendKey(friendId)));
|
||||
if (friendData) {
|
||||
friends.push(friendData as Friend);
|
||||
try {
|
||||
const friend = typeof friendData === 'string' ? JSON.parse(friendData) : friendData;
|
||||
friends.push(friend as Friend);
|
||||
} catch (error) {
|
||||
console.error('解析好友数据失败:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -664,9 +746,9 @@ export class UpstashRedisStorage implements IStorage {
|
|||
}
|
||||
|
||||
async addFriend(userName: string, friend: Friend): Promise<void> {
|
||||
// 保存好友详情
|
||||
// 保存好友详情 - 使用JSON序列化
|
||||
await withRetry(() =>
|
||||
this.client.set(this.friendKey(friend.id), friend)
|
||||
this.client.set(this.friendKey(friend.id), JSON.stringify(friend))
|
||||
);
|
||||
|
||||
// 将好友ID添加到用户的好友列表中
|
||||
|
|
@ -688,11 +770,15 @@ export class UpstashRedisStorage implements IStorage {
|
|||
async updateFriendStatus(friendId: string, status: Friend['status']): Promise<void> {
|
||||
const friendData = await withRetry(() => this.client.get(this.friendKey(friendId)));
|
||||
if (friendData) {
|
||||
const friend = friendData as Friend;
|
||||
try {
|
||||
const friend = typeof friendData === 'string' ? JSON.parse(friendData) : friendData as Friend;
|
||||
friend.status = status;
|
||||
await withRetry(() =>
|
||||
this.client.set(this.friendKey(friendId), friend)
|
||||
this.client.set(this.friendKey(friendId), JSON.stringify(friend))
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('更新好友状态失败:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -706,11 +792,15 @@ export class UpstashRedisStorage implements IStorage {
|
|||
for (const requestId of requestIds) {
|
||||
const requestData = await withRetry(() => this.client.get(this.friendRequestKey(requestId)));
|
||||
if (requestData) {
|
||||
const request = requestData as FriendRequest;
|
||||
try {
|
||||
const request = typeof requestData === 'string' ? JSON.parse(requestData) : requestData as FriendRequest;
|
||||
// 只返回相关的申请(发送给该用户的或该用户发送的)
|
||||
if (request.to_user === userName || request.from_user === userName) {
|
||||
requests.push(request);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('解析好友申请失败:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -718,9 +808,9 @@ export class UpstashRedisStorage implements IStorage {
|
|||
}
|
||||
|
||||
async createFriendRequest(request: FriendRequest): Promise<void> {
|
||||
// 保存申请详情
|
||||
// 保存申请详情 - 使用JSON序列化
|
||||
await withRetry(() =>
|
||||
this.client.set(this.friendRequestKey(request.id), request)
|
||||
this.client.set(this.friendRequestKey(request.id), JSON.stringify(request))
|
||||
);
|
||||
|
||||
// 将申请ID添加到双方的申请列表中
|
||||
|
|
@ -735,19 +825,24 @@ export class UpstashRedisStorage implements IStorage {
|
|||
async updateFriendRequest(requestId: string, status: FriendRequest['status']): Promise<void> {
|
||||
const requestData = await withRetry(() => this.client.get(this.friendRequestKey(requestId)));
|
||||
if (requestData) {
|
||||
const request = requestData as FriendRequest;
|
||||
try {
|
||||
const request = typeof requestData === 'string' ? JSON.parse(requestData) : requestData as FriendRequest;
|
||||
request.status = status;
|
||||
request.updated_at = Date.now();
|
||||
await withRetry(() =>
|
||||
this.client.set(this.friendRequestKey(requestId), request)
|
||||
this.client.set(this.friendRequestKey(requestId), JSON.stringify(request))
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('更新好友申请失败:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async deleteFriendRequest(requestId: string): Promise<void> {
|
||||
const requestData = await withRetry(() => this.client.get(this.friendRequestKey(requestId)));
|
||||
if (requestData) {
|
||||
const request = requestData as FriendRequest;
|
||||
try {
|
||||
const request = typeof requestData === 'string' ? JSON.parse(requestData) : requestData as FriendRequest;
|
||||
|
||||
// 从双方的申请列表中移除
|
||||
await withRetry(() =>
|
||||
|
|
@ -756,6 +851,9 @@ export class UpstashRedisStorage implements IStorage {
|
|||
await withRetry(() =>
|
||||
this.client.srem(this.userFriendRequestsKey(request.to_user), requestId)
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('删除好友申请失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 删除申请详情
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/* eslint-disable no-console */
|
||||
|
||||
const CURRENT_VERSION = '8.9.0';
|
||||
const CURRENT_VERSION = '8.9.5';
|
||||
|
||||
// 导出当前版本号供其他地方使用
|
||||
export { CURRENT_VERSION };
|
||||
|
|
|
|||
|
|
@ -133,6 +133,6 @@ function shouldSkipAuth(pathname: string): boolean {
|
|||
// 配置middleware匹配规则
|
||||
export const config = {
|
||||
matcher: [
|
||||
'/((?!_next/static|_next/image|favicon.ico|login|warning|api/login|api/register|api/logout|api/cron|api/server-config|api/theme).*)',
|
||||
'/((?!_next/static|_next/image|favicon.ico|login|warning|api/login|api/register|api/logout|api/cron|api/server-config).*)',
|
||||
],
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue