// ifinance 外接模块 (宿主侧) // ------------------------------------------------------------ // ifinance 是静态托管模块包, 唯一索引是 info.json。宿主约定: // - 根地址固定为 https://ifinance.honor3.com (宿主常量, 不写进 info.json) // - 没有统一入口页, 每个 html 都是独立入口 (在 WebView/iframe 中打开) // - categories[] 对应 App 的 tab, items[] 是条目; 未知字段忽略 // // 本模块采用「纯前端直连」方式: 直接 fetch 远端 info.json, 条目 HTML 直接 // 用 iframe 加载远端地址, 不做端上文件缓存 (始终最新)。 // 开发环境 (vite dev) 走 /ifinance-api 代理以规避跨域; 生产环境直连。 // 若远端 info.json 拉取失败, 依次回退: 同源内置快照 (public/ifinance-info.json) // → localStorage 上次成功快照, 保证分类 tab 仍可渲染。 import { useEffect, useState } from 'react' // 根地址: 生产直连; 开发用 vite 代理前缀 (见 vite.config.js) export const FINANCE_BASE = import.meta.env.DEV ? '/ifinance-api' : 'https://ifinance.honor3.com' // 同源兜底快照 (构建时由 ifinance/info.json 拷贝到 public/, 仅在远端不可用时使用) const FALLBACK_INFO_URL = '/ifinance-info.json' const INFO_CACHE_KEY = 'ifinance_info_v1' // 模块级缓存: 一次会话内多处 (宏观页 / 详情页) 共享, 避免重复请求 let memoInfo = null let inflight = null // 拼接 info.json 中给出的相对路径 (如 public/econ/x.html) 为可加载的绝对 URL export function financeUrl(relPath) { const p = String(relPath || '').replace(/^\/+/, '') return FINANCE_BASE + '/' + p } // 条目 HTML 地址: Cloudflare Pages 走 clean URL, 去掉 .html 后缀直达 (避免 307 重定向) export function financePageUrl(relPath) { return financeUrl(String(relPath || '').replace(/\.html?$/i, '')) } function readLocalInfo() { try { const raw = localStorage.getItem(INFO_CACHE_KEY) if (!raw) return null const parsed = JSON.parse(raw) if (parsed && parsed.info && Array.isArray(parsed.info.categories)) return parsed.info } catch { /* ignore */ } return null } function writeLocalInfo(info) { try { localStorage.setItem(INFO_CACHE_KEY, JSON.stringify({ ts: Date.now(), info })) } catch { /* 配额满等忽略 */ } } async function fetchInfoFrom(url) { const res = await fetch(url, { cache: 'no-cache' }) if (!res.ok) throw new Error('ifinance 索引加载失败 (' + res.status + ')') const info = await res.json() if (!info || !Array.isArray(info.categories)) throw new Error('ifinance 索引格式无效') return info } // 拉取 (并缓存) info.json: 远端优先, 失败回退同源内置快照 export async function fetchIfinanceInfo() { if (inflight) return inflight inflight = (async () => { let info try { info = await fetchInfoFrom(financeUrl('info.json')) } catch (remoteErr) { // 兜底: 同源内置快照 (ifinance 未开 CORS / 临时不可达时仍可渲染分类) try { info = await fetchInfoFrom(FALLBACK_INFO_URL) } catch { throw remoteErr } } memoInfo = info writeLocalInfo(info) return info })() try { return await inflight } finally { inflight = null } } // React Hook: 获取分类索引。优先内存 → localStorage 快照 → 远端 export function useIfinanceInfo() { const [state, setState] = useState(() => { if (memoInfo) return { info: memoInfo, loading: false, error: '' } const cached = readLocalInfo() return { info: cached, loading: true, error: '' } }) useEffect(() => { let alive = true setState((s) => ({ ...s, loading: true })) fetchIfinanceInfo() .then((info) => { if (alive) setState({ info, loading: false, error: '' }) }) .catch((e) => { if (!alive) return const cached = readLocalInfo() setState({ info: cached, loading: false, error: cached ? '' : (e?.message || '外接模块加载失败'), }) }) return () => { alive = false } }, []) return state }