mirror of
https://github.com/snowykami/neo-blog.git
synced 2025-09-26 11:06:23 +00:00
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:
160
web/src/contexts/site-info-context.tsx
Normal file
160
web/src/contexts/site-info-context.tsx
Normal 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;
|
Reference in New Issue
Block a user