/gi) || [];
for (const r of rows) {
const cells = [...r.matchAll(/([\s\S]*?)<\/td>/gi)].map((m) => stripHtml(m[1]));
if (!cells.length) continue;
const codeIdx = cells.findIndex((c) => /^\d{6}$/.test(c));
const weightIdx = cells.findIndex((c) => /\d+(?:\.\d+)?\s*%/.test(c));
const code = codeIdx >= 0 ? cells[codeIdx] : null;
const name = codeIdx >= 0 && codeIdx + 1 < cells.length ? cells[codeIdx + 1] : null;
const weight = weightIdx >= 0 ? cells[weightIdx].replace(/\s+/g, '') : null;
if (code && (name || name === '') && weight) {
list.push({ code, name, weight });
} else {
const anchorNameMatch = r.match(/]*?>([^<]+)<\/a>/i);
const altName = anchorNameMatch ? stripHtml(anchorNameMatch[1]) : null;
const codeMatch = r.match(/(\d{6})/);
const weightMatch = r.match(/(\d+(?:\.\d+)?)\s*%/);
const fallbackCode = codeMatch ? codeMatch[1] : null;
const fallbackWeight = weightMatch ? `${weightMatch[1]}%` : null;
if ((code || fallbackCode) && (name || altName) && (weight || fallbackWeight)) {
list.push({ code: code || fallbackCode, name: name || altName, weight: weight || fallbackWeight });
}
}
}
return list.slice(0, 10);
};
const getHoldingsByJsonp = async (c) => {
return new Promise(async (resolve, reject) => {
try {
// Eastmoney 返回形如 var apidata={content:''}
// 先清理可能残留的变量
delete window.apidata;
await loadScript(`https://fundf10.eastmoney.com/FundArchivesDatas.aspx?type=jjcc&code=${encodeURIComponent(c)}&topline=10&year=&month=&rt=${Date.now()}`);
const content = window.apidata?.content || '';
const m = content.match(//i) || content.match(/content:\s*'([\s\S]*?)'/i);
const html = m ? (m[0].startsWith(' {
if (useJsonp) {
const gz = await getFundGZByJsonp(c);
const holdings = await getHoldingsByJsonp(c);
return { ...gz, holdings };
}
const res = await fetch(buildUrl(`/fund?code=${encodeURIComponent(c)}`), { cache: 'no-store' });
if (!res.ok) throw new Error('网络错误');
return await res.json();
};
const refreshAll = async (codes) => {
try {
const updated = await Promise.all(
codes.map((c) => fetchFundData(c))
);
setFunds(updated);
localStorage.setItem('funds', JSON.stringify(updated));
} catch (e) {
console.error(e);
}
};
const addFund = async (e) => {
e.preventDefault();
setError('');
const clean = code.trim();
if (!clean) {
setError('请输入基金编号');
return;
}
if (funds.some((f) => f.code === clean)) {
setError('该基金已添加');
return;
}
setLoading(true);
try {
const data = await fetchFundData(clean);
const next = [data, ...funds];
setFunds(next);
localStorage.setItem('funds', JSON.stringify(next));
setCode('');
} catch (e) {
setError(e.message || '添加失败');
} finally {
setLoading(false);
}
};
const removeFund = (removeCode) => {
const next = funds.filter((f) => f.code !== removeCode);
setFunds(next);
localStorage.setItem('funds', JSON.stringify(next));
};
const manualRefresh = async () => {
if (manualRefreshing) return;
const codes = funds.map((f) => f.code);
if (!codes.length) return;
setManualRefreshing(true);
try {
await refreshAll(codes);
} finally {
setManualRefreshing(false);
}
};
const saveSettings = (e) => {
e?.preventDefault?.();
const ms = Math.max(5, Number(tempSeconds)) * 1000;
setRefreshMs(ms);
localStorage.setItem('refreshMs', String(ms));
const nextApi = (tempApiBase || '/api').trim();
setApiBase(nextApi);
localStorage.setItem('apiBase', nextApi);
localStorage.setItem('useJsonp', useJsonp ? 'true' : 'false');
setSettingsOpen(false);
};
useEffect(() => {
const onKey = (ev) => {
if (ev.key === 'Escape' && settingsOpen) setSettingsOpen(false);
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [settingsOpen]);
return (
刷新
{Math.round(refreshMs / 1000)}秒
{funds.length === 0 ? (
尚未添加基金
) : (
{funds.map((f) => (
{f.name}
#{f.code}
估值时间
{f.gztime || f.time || '-'}
前10重仓股票
持仓占比
{Array.isArray(f.holdings) && f.holdings.length ? (
{f.holdings.map((h, idx) => (
{h.name ? h.name : h.code}
{h.code ? ` (${h.code})` : ''}
{h.weight}
))}
) : (
暂无重仓数据
)}
))}
)}
数据源:基金估值与重仓来自东方财富公开接口,可能存在延迟
{settingsOpen && (
setSettingsOpen(false)}>
e.stopPropagation()}>
设置
数据源与刷新频率
数据源
跨域环境推荐 JSONP 直连
{[
{ key: 'api', label: '后端 API(推荐)', active: !useJsonp },
{ key: 'jsonp', label: 'JSONP 直连', active: useJsonp }
].map((opt) => (
))}
{!useJsonp && (
setTempApiBase(e.target.value)}
placeholder="/api 或 https://你的域名/api"
/>
)}
{[10, 30, 60, 120, 300].map((s) => (
))}
)}
);
}
|