diff --git a/.gitignore b/.gitignore index d4be12c..9cac844 100644 --- a/.gitignore +++ b/.gitignore @@ -79,3 +79,4 @@ fabric.properties /node_modules/ /.next/ +.vscode/ diff --git a/app/page.jsx b/app/page.jsx index acc3c47..f4bc7f0 100644 --- a/app/page.jsx +++ b/app/page.jsx @@ -228,7 +228,7 @@ function DatePicker({ value, onChange }) { onClick={handleNextMonth} className="icon-button" style={{ width: 24, height: 24 }} - // 如果下个月已经是未来,可以禁用(可选,这里简单起见不禁用翻页,只禁用日期点击) + // 如果下个月已经是未来,可以禁用(可选,这里简单起见不禁用翻页,只禁用日期点击) > > @@ -1808,60 +1808,60 @@ export default function HomePage() { return () => clearInterval(timer); }, []); - // 计算持仓收益 - const getHoldingProfit = (fund, holding) => { - if (!holding || typeof holding.share !== 'number') return null; + // 计算持仓收益 + const getHoldingProfit = (fund, holding) => { + if (!holding || typeof holding.share !== 'number') return null; - const now = new Date(); - const isAfter9 = now.getHours() >= 9; - const hasTodayData = fund.jzrq === todayStr; + const now = new Date(); + const isAfter9 = now.getHours() >= 9; + const hasTodayData = fund.jzrq === todayStr; - // 如果是交易日且9点以后,且今日净值未出,则强制使用估值(隐藏涨跌幅列模式) - const useValuation = isTradingDay && isAfter9 && !hasTodayData; + // 如果是交易日且9点以后,且今日净值未出,则强制使用估值(隐藏涨跌幅列模式) + const useValuation = isTradingDay && isAfter9 && !hasTodayData; - let currentNav; - let profitToday; + let currentNav; + let profitToday; - if (!useValuation) { - // 使用确权净值 (dwjz) - currentNav = Number(fund.dwjz); - if (!currentNav) return null; + if (!useValuation) { + // 使用确权净值 (dwjz) + currentNav = Number(fund.dwjz); + if (!currentNav) return null; - const amount = holding.share * currentNav; - // 优先用 zzl (真实涨跌幅), 降级用 gszzl - const rate = fund.zzl !== undefined ? Number(fund.zzl) : (Number(fund.gszzl) || 0); - profitToday = amount - (amount / (1 + rate / 100)); - } else { - // 否则使用估值 - currentNav = fund.estPricedCoverage > 0.05 - ? fund.estGsz - : (typeof fund.gsz === 'number' ? fund.gsz : Number(fund.dwjz)); + const amount = holding.share * currentNav; + // 优先用 zzl (真实涨跌幅), 降级用 gszzl + const rate = fund.zzl !== undefined ? Number(fund.zzl) : (Number(fund.gszzl) || 0); + profitToday = amount - (amount / (1 + rate / 100)); + } else { + // 否则使用估值 + currentNav = fund.estPricedCoverage > 0.05 + ? fund.estGsz + : (typeof fund.gsz === 'number' ? fund.gsz : Number(fund.dwjz)); - if (!currentNav) return null; + if (!currentNav) return null; - const amount = holding.share * currentNav; - // 估值涨跌幅 - const gzChange = fund.estPricedCoverage > 0.05 ? fund.estGszzl : (Number(fund.gszzl) || 0); - profitToday = amount - (amount / (1 + gzChange / 100)); - } + const amount = holding.share * currentNav; + // 估值涨跌幅 + const gzChange = fund.estPricedCoverage > 0.05 ? fund.estGszzl : (Number(fund.gszzl) || 0); + profitToday = amount - (amount / (1 + gzChange / 100)); + } - // 持仓金额 - const amount = holding.share * currentNav; + // 持仓金额 + const amount = holding.share * currentNav; - // 总收益 = (当前净值 - 成本价) * 份额 - const profitTotal = typeof holding.cost === 'number' - ? (currentNav - holding.cost) * holding.share - : null; + // 总收益 = (当前净值 - 成本价) * 份额 + const profitTotal = typeof holding.cost === 'number' + ? (currentNav - holding.cost) * holding.share + : null; - return { - amount, - profitToday, - profitTotal - }; + return { + amount, + profitToday, + profitTotal }; + }; - // 过滤和排序后的基金列表 + // 过滤和排序后的基金列表 const displayFunds = funds .filter(f => { if (currentTab === 'all') return true; @@ -1869,7 +1869,7 @@ export default function HomePage() { const group = groups.find(g => g.id === currentTab); return group ? group.codes.includes(f.code) : true; }) - .sort((a, b) => { + .sort((a, b) => { if (sortBy === 'yield') { const valA = typeof a.estGszzl === 'number' ? a.estGszzl : (Number(a.gszzl) || 0); const valB = typeof b.estGszzl === 'number' ? b.estGszzl : (Number(b.gszzl) || 0); @@ -1882,7 +1882,7 @@ export default function HomePage() { const valB = pb?.profitTotal ?? Number.NEGATIVE_INFINITY; return sortOrder === 'asc' ? valA - valB : valB - valA; } - if(sortBy === 'name'){ + if (sortBy === 'name') { return sortOrder === 'asc' ? a.name.localeCompare(b.name, 'zh-CN') : b.name.localeCompare(a.name, 'zh-CN'); } return 0; @@ -2170,7 +2170,7 @@ export default function HomePage() { if (savedHoldings && typeof savedHoldings === 'object') { setHoldings(savedHoldings); } - } catch {} + } catch { } }, []); useEffect(() => { @@ -2202,6 +2202,97 @@ export default function HomePage() { }); }; + // 当估值接口无法获取数据时,使用腾讯接口获取基金基本信息和净值(回退方案) + const fetchFundDataFallback = async (c) => { + return new Promise(async (resolve, reject) => { + // 先通过东方财富搜索接口获取基金名称 + const searchCallbackName = `SuggestData_fallback_${Date.now()}`; + const searchUrl = `https://fundsuggest.eastmoney.com/FundSearch/api/FundSearchAPI.ashx?m=1&key=${encodeURIComponent(c)}&callback=${searchCallbackName}&_=${Date.now()}`; + + let fundName = ''; + + try { + await new Promise((resSearch, rejSearch) => { + window[searchCallbackName] = (data) => { + if (data && data.Datas && data.Datas.length > 0) { + const found = data.Datas.find(d => d.CODE === c); + if (found) { + fundName = found.NAME || found.SHORTNAME || ''; + } + } + delete window[searchCallbackName]; + resSearch(); + }; + + const script = document.createElement('script'); + script.src = searchUrl; + script.async = true; + script.onload = () => { + if (document.body.contains(script)) document.body.removeChild(script); + }; + script.onerror = () => { + if (document.body.contains(script)) document.body.removeChild(script); + delete window[searchCallbackName]; + rejSearch(new Error('搜索接口失败')); + }; + document.body.appendChild(script); + + // 超时处理 + setTimeout(() => { + if (window[searchCallbackName]) { + delete window[searchCallbackName]; + resSearch(); + } + }, 3000); + }); + } catch (e) { + // 搜索失败,继续尝试腾讯接口 + } + + // 使用腾讯接口获取净值数据 + const tUrl = `https://qt.gtimg.cn/q=jj${c}`; + const tScript = document.createElement('script'); + tScript.src = tUrl; + tScript.onload = () => { + const v = window[`v_jj${c}`]; + if (v && v.length > 5) { + const p = v.split('~'); + // p[1]: 基金名称, p[5]: 单位净值, p[7]: 涨跌幅, p[8]: 净值日期 + const name = fundName || p[1] || `未知基金(${c})`; + const dwjz = p[5]; + const zzl = parseFloat(p[7]); + const jzrq = p[8] ? p[8].slice(0, 10) : ''; + + if (dwjz) { + // 成功获取净值数据 + resolve({ + code: c, + name: name, + dwjz: dwjz, + gsz: null, // 无估值数据 + gztime: null, + jzrq: jzrq, + gszzl: null, // 无估值涨跌幅 + zzl: !isNaN(zzl) ? zzl : null, + noValuation: true, // 标记为无估值数据 + holdings: [] + }); + } else { + reject(new Error('未能获取到基金数据')); + } + } else { + reject(new Error('未能获取到基金数据')); + } + if (document.body.contains(tScript)) document.body.removeChild(tScript); + }; + tScript.onerror = () => { + if (document.body.contains(tScript)) document.body.removeChild(tScript); + reject(new Error('基金数据加载失败')); + }; + document.body.appendChild(tScript); + }); + }; + const fetchFundData = async (c) => { return new Promise(async (resolve, reject) => { // 腾讯接口识别逻辑优化 @@ -2226,7 +2317,8 @@ export default function HomePage() { window.jsonpgz = (json) => { window.jsonpgz = originalJsonpgz; // 立即恢复 if (!json || typeof json !== 'object') { - reject(new Error('未获取到基金估值数据')); + // 估值数据无法获取时,尝试使用腾讯接口获取基金基本信息和净值 + fetchFundDataFallback(c).then(resolve).catch(reject); return; } const gszzlNum = Number(json.gszzl); @@ -2365,7 +2457,7 @@ export default function HomePage() { }; document.body.appendChild(scriptQuote); }); - } catch (e) {} + } catch (e) { } } resolveH(holdings); }).catch(() => resolveH([])); @@ -2879,7 +2971,7 @@ export default function HomePage() { 基估宝