From e2d8858432260bff296ca1ace8f4380476d65321 Mon Sep 17 00:00:00 2001
From: hzm <934585316@qq.com>
Date: Tue, 3 Mar 2026 20:19:52 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E5=89=8D10=E9=87=8D=E4=BB=93=E8=82=A1?=
=?UTF-8?q?=E7=A5=A8=E5=8D=A0=E6=AF=94=E5=B1=95=E7=A4=BA=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/api/fund.js | 203 +++++++++++++++++++++++++++---------------------
1 file changed, 114 insertions(+), 89 deletions(-)
diff --git a/app/api/fund.js b/app/api/fund.js
index 77efa50..07c7c52 100644
--- a/app/api/fund.js
+++ b/app/api/fund.js
@@ -305,97 +305,122 @@ export const fetchFundData = async (c) => {
.catch(() => resolveT(null));
});
const holdingsPromise = new Promise((resolveH) => {
- (async () => {
- try {
- const pz = await fetchFundPingzhongdata(c);
- const rawCodes = Array.isArray(pz?.stockCodes) ? pz.stockCodes : [];
- const codes = rawCodes
- .map((code) => String(code).slice(0, 6))
- .filter((code) => /^\d{6}$/.test(code))
- .slice(0, 10);
+ const holdingsUrl = `https://fundf10.eastmoney.com/FundArchivesDatas.aspx?type=jjcc&code=${c}&topline=10&year=&month=&_=${Date.now()}`;
+ const holdingsCacheKey = `fund_holdings_archives_${c}`;
+ cachedRequest(
+ () => loadScript(holdingsUrl),
+ holdingsCacheKey,
+ { cacheTime: 60 * 60 * 1000 }
+ ).then(async (apidata) => {
+ let holdings = [];
+ const html = apidata?.content || '';
+ const holdingsReportDate = extractHoldingsReportDate(html);
+ const holdingsIsLastQuarter = isLastQuarterReport(holdingsReportDate);
- if (!codes.length) {
- resolveH({ holdings: [], holdingsReportDate: null, holdingsIsLastQuarter: false });
- return;
- }
-
- let holdings = codes.map((code) => ({
- code,
- name: '',
- weight: '',
- change: null
- }));
-
- const needQuotes = holdings.filter(h => /^\d{6}$/.test(h.code) || /^\d{5}$/.test(h.code));
- if (needQuotes.length) {
- try {
- const tencentCodes = needQuotes.map(h => {
- const cd = String(h.code || '');
- if (/^\d{6}$/.test(cd)) {
- const pfx = cd.startsWith('6') || cd.startsWith('9') ? 'sh' : ((cd.startsWith('4') || cd.startsWith('8')) ? 'bj' : 'sz');
- return `s_${pfx}${cd}`;
- }
- if (/^\d{5}$/.test(cd)) {
- return `s_hk${cd}`;
- }
- return null;
- }).filter(Boolean).join(',');
- if (tencentCodes) {
- const quoteUrl = `https://qt.gtimg.cn/q=${tencentCodes}`;
- await new Promise((resQuote) => {
- const scriptQuote = document.createElement('script');
- scriptQuote.src = quoteUrl;
- scriptQuote.onload = () => {
- needQuotes.forEach(h => {
- const cd = String(h.code || '');
- let varName = '';
- if (/^\d{6}$/.test(cd)) {
- const pfx = cd.startsWith('6') || cd.startsWith('9') ? 'sh' : ((cd.startsWith('4') || cd.startsWith('8')) ? 'bj' : 'sz');
- varName = `v_s_${pfx}${cd}`;
- } else if (/^\d{5}$/.test(cd)) {
- varName = `v_s_hk${cd}`;
- } else {
- return;
- }
- const dataStr = window[varName];
- if (dataStr) {
- const parts = dataStr.split('~');
- if (parts.length > 5) {
- // parts[1] 是名称,parts[5] 是涨跌幅
- if (!h.name && parts[1]) {
- h.name = parts[1];
- }
- const chg = parseFloat(parts[5]);
- if (!Number.isNaN(chg)) {
- h.change = chg;
- }
- }
- }
- });
- 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) {
- }
- }
-
- // 使用 pingzhongdata 的结果作为展现依据:有前 10 代码即视为可展示
- resolveH({
- holdings,
- holdingsReportDate: null,
- holdingsIsLastQuarter: holdings.length > 0
- });
- } catch (e) {
- resolveH({ holdings: [], holdingsReportDate: null, holdingsIsLastQuarter: false });
+ // 如果不是上一季度末的披露数据,则不展示重仓(并避免继续解析/请求行情)
+ if (!holdingsIsLastQuarter) {
+ resolveH({ holdings: [], holdingsReportDate, holdingsIsLastQuarter: false });
+ return;
}
- })();
+
+ const headerRow = (html.match(/[\s\S]*?<\/thead>/i) || [])[0] || '';
+ const headerCells = (headerRow.match(/([\s\S]*?)<\/th>/gi) || []).map(th => th.replace(/<[^>]*>/g, '').trim());
+ let idxCode = -1, idxName = -1, idxWeight = -1;
+ headerCells.forEach((h, i) => {
+ const t = h.replace(/\s+/g, '');
+ if (idxCode < 0 && (t.includes('股票代码') || t.includes('证券代码'))) idxCode = i;
+ if (idxName < 0 && (t.includes('股票名称') || t.includes('证券名称'))) idxName = i;
+ if (idxWeight < 0 && (t.includes('占净值比例') || t.includes('占比'))) idxWeight = i;
+ });
+ const rows = html.match(/