mirror of
https://github.com/snowykami/neo-blog.git
synced 2025-09-26 02:56:22 +00:00
160 lines
4.5 KiB
TypeScript
160 lines
4.5 KiB
TypeScript
"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",
|
||
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] = mergeWithFallback(initialValue as Record<string, unknown>, fallbackValue as Record<string, unknown>) as T[typeof key];
|
||
}
|
||
// 否则保持 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; |