/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);
-}
-
-async function fetchHoldings(code) {
- const url = `https://fundf10.eastmoney.com/FundArchivesDatas.aspx?type=jjcc&code=${code}&topline=10&year=&month=&rt=${Date.now()}`;
- const res = await fetch(url, {
- headers: {
- 'User-Agent': 'Mozilla/5.0',
- 'Accept': '*/*'
- },
- cache: 'no-store'
- });
- if (!res.ok) throw new Error('重仓接口异常');
- const text = await res.text();
- // The response wraps HTML in var apdfund_...=...; try to extract inner HTML
- const m = text.match(//i) || text.match(/content:\s*'([\s\S]*?)'/i);
- const html = m ? (m[0].startsWith('
-
-
-
+
+
+
);
}
@@ -62,31 +62,12 @@ export default function HomePage() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const timerRef = useRef(null);
+ const [manualRefreshing, setManualRefreshing] = useState(false);
+
+ // 刷新频率状态
const [refreshMs, setRefreshMs] = useState(30000);
const [settingsOpen, setSettingsOpen] = useState(false);
const [tempSeconds, setTempSeconds] = useState(30);
- const [manualRefreshing, setManualRefreshing] = useState(false);
-
- // --- 新增:API 地址与数据源模式 ---
- const [useJsonp, setUseJsonp] = useState(false);
- const [apiBase, setApiBase] = useState('/api');
- const [tempApiBase, setTempApiBase] = useState('/api');
- const [tempUseJsonp, setTempUseJsonp] = useState(false);
-
- // 构造绝对路径的工具函数,避免 GitHub Pages basePath 干扰
- const buildUrl = (path) => {
- // 如果 apiBase 是绝对地址,直接拼接
- if (apiBase.startsWith('http://') || apiBase.startsWith('https://')) {
- const base = apiBase.replace(/\/$/, '');
- const p = path.startsWith('/') ? path : `/${path}`;
- return `${base}${p}`;
- }
- // 否则拼接当前 origin 确保是绝对地址
- const origin = typeof window !== 'undefined' ? window.location.origin : '';
- const base = apiBase.startsWith('/') ? apiBase : `/${apiBase}`;
- const p = path.startsWith('/') ? path : `/${path}`;
- return `${origin}${base.replace(/\/$/, '')}${p}`;
- };
useEffect(() => {
try {
@@ -96,16 +77,10 @@ export default function HomePage() {
refreshAll(saved.map((f) => f.code));
}
const savedMs = parseInt(localStorage.getItem('refreshMs') || '30000', 10);
- if (Number.isFinite(savedMs) && savedMs > 0) {
+ if (Number.isFinite(savedMs) && savedMs >= 5000) {
setRefreshMs(savedMs);
setTempSeconds(Math.round(savedMs / 1000));
}
- const savedApi = localStorage.getItem('apiBase') || '/api';
- setApiBase(savedApi);
- setTempApiBase(savedApi);
- const savedJsonp = localStorage.getItem('useJsonp') === 'true';
- setUseJsonp(savedJsonp);
- setTempUseJsonp(savedJsonp);
} catch {}
}, []);
@@ -132,65 +107,138 @@ export default function HomePage() {
};
script.onerror = () => {
document.body.removeChild(script);
- reject(new Error('脚本加载失败'));
+ reject(new Error('数据加载失败'));
};
document.body.appendChild(script);
});
};
const fetchFundData = async (c) => {
- if (useJsonp) {
- // JSONP 模式:直接请求东方财富
- return new Promise(async (resolve, reject) => {
- const gzUrl = `https://fundgz.1234567.com.cn/js/${c}.js?rt=${Date.now()}`;
- // 东方财富估值接口固定回调 jsonpgz
- window.jsonpgz = (json) => {
- const gszzlNum = Number(json.gszzl);
- const gzData = {
- code: json.fundcode,
- name: json.name,
- dwjz: json.dwjz,
- gsz: json.gsz,
- gztime: json.gztime,
- gszzl: Number.isFinite(gszzlNum) ? gszzlNum : json.gszzl
- };
-
- // 获取重仓 (通过脚本注入并解析全局变量)
- const holdingsUrl = `https://fundf10.eastmoney.com/FundArchivesDatas.aspx?type=jjcc&code=${c}&topline=10&year=&month=&rt=${Date.now()}`;
- loadScript(holdingsUrl).then(() => {
- let holdings = [];
- const html = window.apidata?.content || '';
- const rows = html.match(//gi) || [];
- for (const r of rows) {
- const cells = (r.match(/| ([\s\S]*?)<\/td>/gi) || []).map(td => td.replace(/<[^>]*>/g, '').trim());
- const codeIdx = cells.findIndex(txt => /^\d{6}$/.test(txt));
- const weightIdx = cells.findIndex(txt => /\d+(?:\.\d+)?\s*%/.test(txt));
- if (codeIdx >= 0 && weightIdx >= 0) {
- holdings.push({
- code: cells[codeIdx],
- name: cells[codeIdx + 1] || '',
- weight: cells[weightIdx]
- });
- }
- }
- resolve({ ...gzData, holdings: holdings.slice(0, 10) });
- }).catch(() => resolve({ ...gzData, holdings: [] }));
+ return new Promise(async (resolve, reject) => {
+ // 腾讯接口识别逻辑优化
+ const getTencentPrefix = (code) => {
+ if (code.startsWith('6') || code.startsWith('9')) return 'sh';
+ if (code.startsWith('0') || code.startsWith('3')) return 'sz';
+ if (code.startsWith('4') || code.startsWith('8')) return 'bj';
+ return 'sz';
+ };
+
+ const gzUrl = `https://fundgz.1234567.com.cn/js/${c}.js?rt=${Date.now()}`;
+
+ // 使用更安全的方式处理全局回调,避免并发覆盖
+ const currentCallback = `jsonpgz_${c}_${Math.random().toString(36).slice(2, 7)}`;
+
+ // 动态拦截并处理 jsonpgz 回调
+ const scriptGz = document.createElement('script');
+ // 东方财富接口固定调用 jsonpgz,我们通过修改全局变量临时捕获它
+ scriptGz.src = gzUrl;
+
+ const originalJsonpgz = window.jsonpgz;
+ window.jsonpgz = (json) => {
+ window.jsonpgz = originalJsonpgz; // 立即恢复
+ const gszzlNum = Number(json.gszzl);
+ const gzData = {
+ code: json.fundcode,
+ name: json.name,
+ dwjz: json.dwjz,
+ gsz: json.gsz,
+ gztime: json.gztime,
+ gszzl: Number.isFinite(gszzlNum) ? gszzlNum : json.gszzl
};
- loadScript(gzUrl).catch(reject);
- });
- } else {
- // API 模式:使用绝对路径请求后端
- const res = await fetch(buildUrl(`/fund?code=${encodeURIComponent(c)}`), { cache: 'no-store' });
- if (!res.ok) throw new Error('网络错误');
- return await res.json();
- }
+
+ // 获取重仓股票列表
+ const holdingsUrl = `https://fundf10.eastmoney.com/FundArchivesDatas.aspx?type=jjcc&code=${c}&topline=10&year=&month=&rt=${Date.now()}`;
+ loadScript(holdingsUrl).then(async () => {
+ let holdings = [];
+ const html = window.apidata?.content || '';
+ const rows = html.match(/ | /gi) || [];
+ for (const r of rows) {
+ const cells = (r.match(/| ([\s\S]*?)<\/td>/gi) || []).map(td => td.replace(/<[^>]*>/g, '').trim());
+ const codeIdx = cells.findIndex(txt => /^\d{6}$/.test(txt));
+ const weightIdx = cells.findIndex(txt => /\d+(?:\.\d+)?\s*%/.test(txt));
+ if (codeIdx >= 0 && weightIdx >= 0) {
+ holdings.push({
+ code: cells[codeIdx],
+ name: cells[codeIdx + 1] || '',
+ weight: cells[weightIdx],
+ change: null
+ });
+ }
+ }
+
+ holdings = holdings.slice(0, 10);
+
+ if (holdings.length) {
+ try {
+ const tencentCodes = holdings.map(h => `s_${getTencentPrefix(h.code)}${h.code}`).join(',');
+ const quoteUrl = `https://qt.gtimg.cn/q=${tencentCodes}`;
+
+ await new Promise((resQuote) => {
+ const scriptQuote = document.createElement('script');
+ scriptQuote.src = quoteUrl;
+ scriptQuote.onload = () => {
+ holdings.forEach(h => {
+ const varName = `v_s_${getTencentPrefix(h.code)}${h.code}`;
+ const dataStr = window[varName];
+ if (dataStr) {
+ const parts = dataStr.split('~');
+ // parts[5] 是涨跌幅
+ if (parts.length > 5) {
+ h.change = parseFloat(parts[5]);
+ }
+ }
+ });
+ if (document.body.contains(scriptQuote)) document.body.removeChild(scriptQuote);
+ resQuote();
+ };
+ scriptQuote.onerror = () => {
+ if (document.body.contains(scriptQuote)) document.body.removeChild(scriptQuote);
+ resQuote();
+ };
+ document.body.appendChild(scriptQuote);
+ });
+ } catch (e) {
+ console.error('获取股票涨跌幅失败', e);
+ }
+ }
+
+ resolve({ ...gzData, holdings });
+ }).catch(() => resolve({ ...gzData, holdings: [] }));
+ };
+
+ scriptGz.onerror = () => {
+ window.jsonpgz = originalJsonpgz;
+ if (document.body.contains(scriptGz)) document.body.removeChild(scriptGz);
+ reject(new Error('基金数据加载失败'));
+ };
+
+ document.body.appendChild(scriptGz);
+ // 加载完立即移除脚本
+ setTimeout(() => {
+ if (document.body.contains(scriptGz)) document.body.removeChild(scriptGz);
+ }, 5000);
+ });
};
const refreshAll = async (codes) => {
try {
- const updated = await Promise.all(codes.map(c => fetchFundData(c)));
- setFunds(updated);
- localStorage.setItem('funds', JSON.stringify(updated));
+ // 改用串行请求,避免全局回调 jsonpgz 并发冲突
+ const updated = [];
+ for (const c of codes) {
+ try {
+ const data = await fetchFundData(c);
+ updated.push(data);
+ } catch (e) {
+ console.error(`刷新基金 ${c} 失败`, e);
+ // 失败时保留旧数据
+ const old = funds.find(f => f.code === c);
+ if (old) updated.push(old);
+ }
+ }
+ if (updated.length) {
+ setFunds(updated);
+ localStorage.setItem('funds', JSON.stringify(updated));
+ }
} catch (e) {
console.error(e);
}
@@ -245,14 +293,6 @@ export default function HomePage() {
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);
-
- setUseJsonp(tempUseJsonp);
- localStorage.setItem('useJsonp', String(tempUseJsonp));
-
setSettingsOpen(false);
};
@@ -308,23 +348,18 @@ export default function HomePage() {
输入基金编号(例如:110022)
- {error && {error} }
+ {error && {error} }
@@ -334,7 +369,7 @@ export default function HomePage() {
{funds.map((f) => (
-
+
{f.name}
@@ -347,7 +382,6 @@ export default function HomePage() {
前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}
+
+ {h.name}
+
+ {typeof h.change === 'number' && (
+ 0 ? 'up' : h.change < 0 ? 'down' : ''}`} style={{ marginRight: 8 }}>
+ {h.change > 0 ? '+' : ''}{h.change.toFixed(2)}%
+
+ )}
+ {h.weight}
+
))}
@@ -387,14 +425,15 @@ export default function HomePage() {
- 数据源:基金估值与重仓来自东方财富公开接口,可能存在延迟
+ 数据源:实时估值与重仓直连东方财富,无需后端,部署即用
+
{settingsOpen && (
- setSettingsOpen(false)}>
+ setSettingsOpen(false)}>
e.stopPropagation()}>
设置
- 配置刷新频率与数据源
+ 配置刷新频率
@@ -423,41 +462,6 @@ export default function HomePage() {
/>
-
- 数据源模式
-
-
-
-
- {!tempUseJsonp && (
-
- API 基础地址 (绝对路径)
- setTempApiBase(e.target.value)}
- placeholder="/api 或 https://your-backend.com/api"
- />
-
- 部署到 GitHub Pages 时,请填写完整的后端域名地址。
-
-
- )}
-
-
| |