fix: 前10重仓股票判断获取到的数据是否为上一季度

This commit is contained in:
hzm
2026-03-02 08:18:54 +08:00
parent cbb9d2a105
commit 20e101bb65
2 changed files with 126 additions and 55 deletions

View File

@@ -126,6 +126,55 @@ const parseLatestNetValueFromLsjzContent = (content) => {
return null;
};
const extractHoldingsReportDate = (html) => {
if (!html) return null;
// 优先匹配带有“报告期 / 截止日期”等关键字附近的日期
const m1 = html.match(/(报告期|截止日期)[^0-9]{0,20}(\d{4}-\d{2}-\d{2})/);
if (m1) return m1[2];
// 兜底:取文中出现的第一个 yyyy-MM-dd 格式日期
const m2 = html.match(/(\d{4}-\d{2}-\d{2})/);
return m2 ? m2[1] : null;
};
const isLastQuarterReport = (reportDateStr) => {
if (!reportDateStr) return false;
const report = dayjs(reportDateStr, 'YYYY-MM-DD');
if (!report.isValid()) return false;
const now = nowInTz();
const m = now.month(); // 0-11
const q = Math.floor(m / 3); // 当前季度 0-3 => Q1-Q4
let lastQ;
let year;
if (q === 0) {
// 当前为 Q1则上一季度是上一年的 Q4
lastQ = 3;
year = now.year() - 1;
} else {
lastQ = q - 1;
year = now.year();
}
const quarterEnds = [
{ month: 2, day: 31 }, // Q1 -> 03-31
{ month: 5, day: 30 }, // Q2 -> 06-30
{ month: 8, day: 30 }, // Q3 -> 09-30
{ month: 11, day: 31 } // Q4 -> 12-31
];
const { month: endMonth, day: endDay } = quarterEnds[lastQ];
const lastQuarterEnd = dayjs(
`${year}-${String(endMonth + 1).padStart(2, '0')}-${endDay}`,
'YYYY-MM-DD'
);
return report.isSame(lastQuarterEnd, 'day');
};
export const fetchSmartFundNetValue = async (code, startDate) => {
const today = nowInTz().startOf('day');
let current = toTz(startDate).startOf('day');
@@ -199,7 +248,9 @@ export const fetchFundDataFallback = async (c) => {
gszzl: null,
zzl: Number.isFinite(latest.growth) ? latest.growth : null,
noValuation: true,
holdings: []
holdings: [],
holdingsReportDate: null,
holdingsIsLastQuarter: false
});
} else {
reject(new Error('未能获取到基金数据'));
@@ -258,6 +309,15 @@ export const fetchFundData = async (c) => {
loadScript(holdingsUrl).then(async (apidata) => {
let holdings = [];
const html = apidata?.content || '';
const holdingsReportDate = extractHoldingsReportDate(html);
const holdingsIsLastQuarter = isLastQuarterReport(holdingsReportDate);
// 如果不是上一季度末的披露数据,则不展示重仓(并避免继续解析/请求行情)
if (!holdingsIsLastQuarter) {
resolveH({ holdings: [], holdingsReportDate, holdingsIsLastQuarter: false });
return;
}
const headerRow = (html.match(/<thead[\s\S]*?<tr[\s\S]*?<\/tr>[\s\S]*?<\/thead>/i) || [])[0] || '';
const headerCells = (headerRow.match(/<th[\s\S]*?>([\s\S]*?)<\/th>/gi) || []).map(th => th.replace(/<[^>]*>/g, '').trim());
let idxCode = -1, idxName = -1, idxWeight = -1;
@@ -354,10 +414,15 @@ export const fetchFundData = async (c) => {
} catch (e) {
}
}
resolveH(holdings);
}).catch(() => resolveH([]));
resolveH({ holdings, holdingsReportDate, holdingsIsLastQuarter });
}).catch(() => resolveH({ holdings: [], holdingsReportDate: null, holdingsIsLastQuarter: false }));
});
Promise.all([lsjzPromise, holdingsPromise]).then(([tData, holdings]) => {
Promise.all([lsjzPromise, holdingsPromise]).then(([tData, holdingsResult]) => {
const {
holdings,
holdingsReportDate,
holdingsIsLastQuarter
} = holdingsResult || {};
if (tData) {
if (tData.jzrq && (!gzData.jzrq || tData.jzrq >= gzData.jzrq)) {
gzData.dwjz = tData.dwjz;
@@ -365,7 +430,12 @@ export const fetchFundData = async (c) => {
gzData.zzl = tData.zzl;
}
}
resolve({ ...gzData, holdings });
resolve({
...gzData,
holdings,
holdingsReportDate,
holdingsIsLastQuarter
});
});
};
scriptGz.onerror = () => {

View File

@@ -4162,7 +4162,8 @@ export default function HomePage() {
if (shouldHideChange) return null;
const changeLabel = hasTodayData ? '涨跌幅' : (isYesterdayChange ? '昨日涨跌幅' : (isPreviousTradingDay ? '上一交易日涨跌幅' : '涨跌幅'));
// 不再区分“上一交易日涨跌幅”名称,统一使用“昨日涨跌幅”
const changeLabel = hasTodayData ? '涨跌幅' : '昨日涨跌幅';
return (
<Stat
label={changeLabel}
@@ -4274,58 +4275,58 @@ export default function HomePage() {
/>
);
})()}
<div
style={{ marginBottom: 8, cursor: 'pointer', userSelect: 'none' }}
className="title"
onClick={() => toggleCollapse(f.code)}
>
<div className="row" style={{ width: '100%', flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span>前10重仓股票</span>
<ChevronIcon
width="16"
height="16"
className="muted"
style={{
transform: collapsedCodes.has(f.code) ? 'rotate(-90deg)' : 'rotate(0deg)',
transition: 'transform 0.2s ease'
}}
/>
</div>
<span className="muted">涨跌幅 / 占比</span>
</div>
</div>
<AnimatePresence>
{!collapsedCodes.has(f.code) && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.3, ease: 'easeInOut' }}
style={{ overflow: 'hidden' }}
{f.holdingsIsLastQuarter && Array.isArray(f.holdings) && f.holdings.length > 0 && (
<>
<div
style={{ marginBottom: 8, cursor: 'pointer', userSelect: 'none' }}
className="title"
onClick={() => toggleCollapse(f.code)}
>
{Array.isArray(f.holdings) && f.holdings.length ? (
<div className="list">
{f.holdings.map((h, idx) => (
<div className="item" key={idx}>
<span className="name">{h.name}</span>
<div className="values">
{isNumber(h.change) && (
<span className={`badge ${h.change > 0 ? 'up' : h.change < 0 ? 'down' : ''}`} style={{ marginRight: 8 }}>
{h.change > 0 ? '+' : ''}{h.change.toFixed(2)}%
</span>
)}
<span className="weight">{h.weight}</span>
</div>
</div>
))}
<div className="row" style={{ width: '100%', flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<span>前10重仓股票</span>
<ChevronIcon
width="16"
height="16"
className="muted"
style={{
transform: collapsedCodes.has(f.code) ? 'rotate(-90deg)' : 'rotate(0deg)',
transition: 'transform 0.2s ease'
}}
/>
</div>
) : (
<div className="muted" style={{ padding: '8px 0' }}>暂无重仓数据</div>
<span className="muted">涨跌幅 / 占比</span>
</div>
</div>
<AnimatePresence>
{!collapsedCodes.has(f.code) && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.3, ease: 'easeInOut' }}
style={{ overflow: 'hidden' }}
>
<div className="list">
{f.holdings.map((h, idx) => (
<div className="item" key={idx}>
<span className="name">{h.name}</span>
<div className="values">
{isNumber(h.change) && (
<span className={`badge ${h.change > 0 ? 'up' : h.change < 0 ? 'down' : ''}`} style={{ marginRight: 8 }}>
{h.change > 0 ? '+' : ''}{h.change.toFixed(2)}%
</span>
)}
<span className="weight">{h.weight}</span>
</div>
</div>
))}
</div>
</motion.div>
)}
</motion.div>
)}
</AnimatePresence>
</AnimatePresence>
</>
)}
<FundTrendChart
key={`${f.code}-${theme}`}
code={f.code}