import { useEffect, useState } from 'react'; import { api } from '../api/client'; import type { SiteBranding } from '../api/types'; import { getScope } from '../ssr/scope'; export const DEFAULT_BRANDING: SiteBranding = { name: '姜十三论坛', slogan: '拾三一隅,自在交流', description: '', keywords: '', logo_mark: '姜', logo: '', favicon: '', og_image: '', icp_beian: '', icp_beian_url: 'https://beian.miit.gov.cn/', friend_links: [], }; declare global { interface Window { /** 服务端注入的首屏品牌配置(见 embed_static SPA HTML) */ __J13_BRANDING__?: Partial; } } /** SEO / 首页展示用简介:优先 description,其次 slogan */ export function siteMetaDescription(brand: SiteBranding): string { const d = brand.description?.trim() ?? ''; if (d) return d; return brand.slogan?.trim() ?? ''; } function readBootBranding(): SiteBranding | null { if (typeof window === 'undefined') return null; try { const boot = window.__J13_BRANDING__; if (!boot || typeof boot !== 'object') return null; const name = typeof boot.name === 'string' ? boot.name.trim() : ''; if (!name) return null; return { ...DEFAULT_BRANDING, ...boot, name }; } catch { return null; } } let cached: SiteBranding | null = readBootBranding(); let inflight: Promise | null = null; let cacheEpoch = 0; /** force 预热刚写入后,hook 因 epoch 重跑时复用缓存,避免连打两次 API */ let preferCacheOnce = false; const listeners = new Set<() => void>(); /** 当前生效的品牌配置:SSR 读请求作用域,客户端读模块缓存 */ function currentBranding(): SiteBranding | null { return getScope()?.branding ?? cached; } function fetchBranding(): Promise { if (preferCacheOnce && cached) { preferCacheOnce = false; return Promise.resolve(cached); } // SSR +data / 冷启动预热已 seed 品牌配置时首屏直接复用,不重复请求; // 最新配置同步由显式刷新(refetchSiteBranding / ensureSiteBrandingLoaded force)触发 if (cached) return Promise.resolve(cached); if (inflight) return inflight; inflight = api.siteBranding() .then(b => { cached = { ...DEFAULT_BRANDING, ...b }; return cached; }) .catch(() => cached ?? DEFAULT_BRANDING) .finally(() => { inflight = null; }); return inflight; } /** 浏览器标签标题:站点名 - 副标题(标语) */ export function formatDocumentTitle(brand: SiteBranding): string { const name = brand.name.trim(); const subtitle = brand.slogan.trim(); return subtitle ? `${name} - ${subtitle}` : name; } function applyDocumentBrand(brand: SiteBranding) { // 页面级 SEO hook 已接管标题时,勿覆盖 if (!document.documentElement.hasAttribute('data-j13-seo')) { const title = formatDocumentTitle(brand); if (document.title !== title) document.title = title; } let link = document.querySelector('link[rel="icon"]'); if (brand.favicon) { if (!link) { link = document.createElement('link'); link.rel = 'icon'; document.head.appendChild(link); } if (link.href !== new URL(brand.favicon, window.location.origin).href) { link.href = brand.favicon; } } } /** 同步读取已缓存的品牌配置(供 SEO 等非 hook 场景) */ export function getCachedSiteBranding(): SiteBranding { return currentBranding() ?? DEFAULT_BRANDING; } /** 获取站点品牌配置(名称、Logo 等) */ export function useSiteBranding() { const [branding, setBranding] = useState(currentBranding() ?? DEFAULT_BRANDING); const [loading, setLoading] = useState(!currentBranding()); const [epoch, setEpoch] = useState(cacheEpoch); useEffect(() => { const onInvalidate = () => setEpoch(cacheEpoch); listeners.add(onInvalidate); return () => { listeners.delete(onInvalidate); }; }, []); useEffect(() => { let cancelled = false; if (!cached) setLoading(true); fetchBranding() .then(next => { if (cancelled) return; setBranding(next); applyDocumentBrand(next); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [epoch]); return { branding, loading }; } /** 保留当前画面,后台重拉品牌配置(下拉刷新等) */ export function refetchSiteBranding() { cached = null; inflight = null; cacheEpoch += 1; listeners.forEach(fn => fn()); } /** 冷启动 / 预热:确保品牌已写入模块缓存;force 时重拉并在成功后通知 hook */ export async function ensureSiteBrandingLoaded(opts?: { force?: boolean }): Promise { if (!opts?.force && cached) return cached; if (opts?.force) { cached = null; inflight = null; preferCacheOnce = false; } const next = await fetchBranding(); applyDocumentBrand(next); if (opts?.force) { preferCacheOnce = true; cacheEpoch += 1; listeners.forEach(fn => fn()); } return next; } /** 清除缓存并通知已挂载的 hook 重新拉取 */ export function invalidateSiteBrandingCache() { cached = null; cacheEpoch += 1; listeners.forEach(fn => fn()); } /** 用管理端刚保存的值立即更新缓存与文档标题(SSR 时只写请求作用域) */ export function seedSiteBrandingCache(brand: SiteBranding) { const merged: SiteBranding = { ...DEFAULT_BRANDING, ...brand }; const scope = getScope(); if (scope) { scope.branding = merged; return; } cached = merged; applyDocumentBrand(cached); cacheEpoch += 1; listeners.forEach(fn => fn()); }