Refactor site configuration and color scheme management

- Replaced static config with dynamic site info context.
- Updated color scheme handling in various components to use site info.
- Removed deprecated config file and integrated site info fetching.
- Enhanced user preference page to allow color scheme selection.
- Adjusted blog and console components to reflect new site info structure.
- Improved error handling and fallback mechanisms for site info retrieval.
This commit is contained in:
2025-09-26 00:25:34 +08:00
parent 0812e334df
commit f501948f91
42 changed files with 770 additions and 199 deletions

View File

@ -0,0 +1,160 @@
"use client"
import React, { createContext, useContext, useEffect, useState } from "react";
export type SiteInfo = {
colorSchemes: string[];
metadata: {
name?: string;
icon?: string;
description?: string;
};
defaultCover: string;
owner: {
name: string;
description?: string;
motto?: string;
avatar?: string;
gravatarEmail?: string;
};
postsPerPage: number;
commentsPerPage: number;
verifyCodeCoolDown: number;
animationDurationSecond: number;
footer: {
text?: string;
links?: {
text: string;
href: string;
}[];
};
};
// 这里不写类型定义,是让编辑器根据实际内容推断类型
export const fallbackSiteInfo: SiteInfo = {
colorSchemes: ["blue", "green", "orange", "red", "rose", "violet", "yellow"],
metadata: {
name: "Failed to Fetch Name",
icon: "",
description: "Failed to fetch site info from server.",
},
defaultCover: "https://cdn.liteyuki.org/blog/background.png",
owner: {
name: "Site Owner",
description: "The owner of this site",
motto: "This is a default motto.",
avatar: "",
gravatarEmail: "",
},
postsPerPage: 10,
commentsPerPage: 10,
verifyCodeCoolDown: 60,
animationDurationSecond: 0.3,
footer: {
text: "Default footer text",
links: [
{ text: "Home", href: "/" },
{ text: "About", href: "/about" },
],
},
};
type SiteInfoContextValue = {
siteInfo: SiteInfo;
setSiteInfo: (info: SiteInfo) => void;
isLoaded: boolean;
};
const SiteInfoContext = createContext<SiteInfoContextValue | undefined>(undefined);
/**
* 深度合并两个对象,用 fallback 填补 initial 中缺失的字段
*/
function mergeWithFallback<T extends Record<string, unknown>>(initial: T, fallback: T): T {
const result = { ...initial } as T;
for (const key in fallback) {
if (fallback.hasOwnProperty(key)) {
const initialValue = initial[key];
const fallbackValue = fallback[key];
if (initialValue === undefined || initialValue === null) {
// 如果 initial 中没有这个字段,直接使用 fallback
result[key] = fallbackValue;
} else if (
typeof initialValue === 'object' &&
!Array.isArray(initialValue) &&
typeof fallbackValue === 'object' &&
!Array.isArray(fallbackValue)
) {
// 如果都是对象(非数组),也不要递归合并
result[key] = { ...initialValue, ...fallbackValue };
}
// 否则保持 initial 的值不变
}
}
return result;
}
export const SiteInfoProvider: React.FC<{ initialData?: SiteInfo; children: React.ReactNode }> = ({ initialData, children }) => {
// 合并初始数据和 fallback确保所有字段都有值
const mergedInitialData = initialData ? mergeWithFallback(initialData, fallbackSiteInfo) : fallbackSiteInfo;
const [siteInfo, setSiteInfo] = useState<SiteInfo>(mergedInitialData);
const [isLoaded, setIsLoaded] = useState<boolean>(Boolean(initialData));
// If initialData is not provided (rare), you can fetch on client as fallback.
useEffect(() => {
if (initialData) return;
let mounted = true;
(async () => {
try {
const res = await fetch('/api/siteinfo');
if (!mounted) return;
if (res.ok) {
const data = await res.json();
// 同样对客户端获取的数据进行合并
const mergedData = mergeWithFallback(data, fallbackSiteInfo);
setSiteInfo(mergedData);
}
} catch (e) {
// swallow — siteInfo stays with fallback
console.error('fetch siteinfo failed', e);
} finally {
if (mounted) setIsLoaded(true);
}
})();
return () => { mounted = false };
}, [initialData]);
useEffect(() => {
if (initialData) {
const mergedData = mergeWithFallback(initialData, fallbackSiteInfo);
setSiteInfo(mergedData);
setIsLoaded(true);
}
}, [initialData]);
return (
<SiteInfoContext.Provider value={{
siteInfo,
setSiteInfo: (info: SiteInfo) => {
// 当手动设置 siteInfo 时也进行合并
const mergedInfo = mergeWithFallback(info, fallbackSiteInfo);
setSiteInfo(mergedInfo);
},
isLoaded
}}>
{children}
</SiteInfoContext.Provider>
);
};
export function useSiteInfo() {
const ctx = useContext(SiteInfoContext);
if (!ctx) throw new Error('useSiteInfo must be used within SiteInfoProvider');
return ctx;
}
export default SiteInfoContext;