feat: 定投

This commit is contained in:
hzm
2026-02-25 22:33:06 +08:00
parent 5f12e9d900
commit f5edd7bbf8
12 changed files with 869 additions and 143 deletions

View File

@@ -1,6 +1,7 @@
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
import { isString } from 'lodash';
import { cachedRequest, clearCachedRequest } from '../lib/cacheRequest';
dayjs.extend(utc);
@@ -20,7 +21,7 @@ const nowInTz = () => dayjs().tz(TZ);
const toTz = (input) => (input ? dayjs.tz(input, TZ) : nowInTz());
export const loadScript = (url) => {
if (typeof document === 'undefined' || !document.body) return Promise.resolve();
if (typeof document === 'undefined' || !document.body) return Promise.resolve(null);
let cacheKey = url;
try {
@@ -69,9 +70,7 @@ export const loadScript = (url) => {
clearCachedRequest(cacheKey);
throw new Error(result?.error || '数据加载失败');
}
if (typeof window !== 'undefined' && result.apidata !== undefined) {
window.apidata = result.apidata;
}
return result.apidata;
});
};
@@ -79,9 +78,9 @@ export const fetchFundNetValue = async (code, date) => {
if (typeof window === 'undefined') return null;
const url = `https://fundf10.eastmoney.com/F10DataApi.aspx?type=lsjz&code=${code}&page=1&per=1&sdate=${date}&edate=${date}`;
try {
await loadScript(url);
if (window.apidata && window.apidata.content) {
const content = window.apidata.content;
const apidata = await loadScript(url);
if (apidata && apidata.content) {
const content = apidata.content;
if (content.includes('暂无数据')) return null;
const rows = content.split('<tr>');
for (const row of rows) {
@@ -101,6 +100,32 @@ export const fetchFundNetValue = async (code, date) => {
}
};
const parseLatestNetValueFromLsjzContent = (content) => {
if (!content || content.includes('暂无数据')) return null;
const rowMatches = content.match(/<tr[\s\S]*?<\/tr>/gi) || [];
for (const row of rowMatches) {
const cells = row.match(/<td[^>]*>(.*?)<\/td>/gi) || [];
if (!cells.length) continue;
const getText = (td) => td.replace(/<[^>]+>/g, '').trim();
const dateStr = getText(cells[0] || '');
if (!/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) continue;
const navStr = getText(cells[1] || '');
const nav = parseFloat(navStr);
if (!Number.isFinite(nav)) continue;
let growth = null;
for (const c of cells) {
const txt = getText(c);
const m = txt.match(/([-+]?\d+(?:\.\d+)?)\s*%/);
if (m) {
growth = parseFloat(m[1]);
break;
}
}
return { date: dateStr, nav, growth };
}
return null;
};
export const fetchSmartFundNetValue = async (code, startDate) => {
const today = nowInTz().startOf('day');
let current = toTz(startDate).startOf('day');
@@ -157,43 +182,31 @@ export const fetchFundDataFallback = async (c) => {
});
} 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('~');
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('未能获取到基金数据'));
}
try {
const url = `https://fundf10.eastmoney.com/F10DataApi.aspx?type=lsjz&code=${c}&page=1&per=1&sdate=&edate=`;
const apidata = await loadScript(url);
const content = apidata?.content || '';
const latest = parseLatestNetValueFromLsjzContent(content);
if (latest && latest.nav) {
const name = fundName || `未知基金(${c})`;
resolve({
code: c,
name,
dwjz: String(latest.nav),
gsz: null,
gztime: null,
jzrq: latest.date,
gszzl: null,
zzl: Number.isFinite(latest.growth) ? latest.growth : null,
noValuation: true,
holdings: []
});
} else {
reject(new Error('未能获取到基金数据'));
}
if (document.body.contains(tScript)) document.body.removeChild(tScript);
};
tScript.onerror = () => {
if (document.body.contains(tScript)) document.body.removeChild(tScript);
} catch (e) {
reject(new Error('基金数据加载失败'));
};
document.body.appendChild(tScript);
}
});
};
@@ -222,35 +235,29 @@ export const fetchFundData = async (c) => {
jzrq: json.jzrq,
gszzl: Number.isFinite(gszzlNum) ? gszzlNum : json.gszzl
};
const tencentPromise = new Promise((resolveT) => {
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) {
const p = v.split('~');
resolveT({
dwjz: p[5],
zzl: parseFloat(p[7]),
jzrq: p[8] ? p[8].slice(0, 10) : ''
});
} else {
resolveT(null);
}
if (document.body.contains(tScript)) document.body.removeChild(tScript);
};
tScript.onerror = () => {
if (document.body.contains(tScript)) document.body.removeChild(tScript);
resolveT(null);
};
document.body.appendChild(tScript);
const lsjzPromise = new Promise((resolveT) => {
const url = `https://fundf10.eastmoney.com/F10DataApi.aspx?type=lsjz&code=${c}&page=1&per=1&sdate=&edate=`;
loadScript(url)
.then((apidata) => {
const content = apidata?.content || '';
const latest = parseLatestNetValueFromLsjzContent(content);
if (latest && latest.nav) {
resolveT({
dwjz: String(latest.nav),
zzl: Number.isFinite(latest.growth) ? latest.growth : null,
jzrq: latest.date
});
} else {
resolveT(null);
}
})
.catch(() => resolveT(null));
});
const holdingsPromise = new Promise((resolveH) => {
const holdingsUrl = `https://fundf10.eastmoney.com/FundArchivesDatas.aspx?type=jjcc&code=${c}&topline=10&year=&month=&_=${Date.now()}`;
loadScript(holdingsUrl).then(async () => {
loadScript(holdingsUrl).then(async (apidata) => {
let holdings = [];
const html = window.apidata?.content || '';
const html = apidata?.content || '';
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;
@@ -350,7 +357,7 @@ export const fetchFundData = async (c) => {
resolveH(holdings);
}).catch(() => resolveH([]));
});
Promise.all([tencentPromise, holdingsPromise]).then(([tData, holdings]) => {
Promise.all([lsjzPromise, holdingsPromise]).then(([tData, holdings]) => {
if (tData) {
if (tData.jzrq && (!gzData.jzrq || tData.jzrq >= gzData.jzrq)) {
gzData.dwjz = tData.dwjz;
@@ -499,7 +506,7 @@ export const extractFundNamesWithLLM = async (ocrText) => {
const data = await resp.json();
let content = data?.choices?.[0]?.message?.content?.match(/\{[\s\S]*?\}/)?.[0];
if (!content || typeof content !== 'string') return [];
if (!isString(content)) return [];
let parsed;
try {
@@ -511,7 +518,7 @@ export const extractFundNamesWithLLM = async (ocrText) => {
const names = parsed?.fund_names;
if (!Array.isArray(names)) return [];
return names
.map((n) => (typeof n === 'string' ? n.trim().replaceAll(' ','') : ''))
.map((n) => (isString(n) ? n.trim().replaceAll(' ','') : ''))
.filter(Boolean);
} catch (e) {
return [];
@@ -566,26 +573,26 @@ export const fetchFundHistory = async (code, range = '1m') => {
// Fetch first page to get metadata
const firstUrl = `https://fundf10.eastmoney.com/F10DataApi.aspx?type=lsjz&code=${code}&page=${page}&per=${per}&sdate=${sdate}&edate=${edate}`;
await loadScript(firstUrl);
const firstApidata = await loadScript(firstUrl);
if (!window.apidata || !window.apidata.content || window.apidata.content.includes('暂无数据')) {
if (!firstApidata || !firstApidata.content || firstApidata.content.includes('暂无数据')) {
resolve([]);
return;
}
// Parse total pages
if (window.apidata.pages) {
totalPages = parseInt(window.apidata.pages, 10) || 1;
if (firstApidata.pages) {
totalPages = parseInt(firstApidata.pages, 10) || 1;
}
allData = allData.concat(parseContent(window.apidata.content));
allData = allData.concat(parseContent(firstApidata.content));
// Fetch remaining pages
for (page = 2; page <= totalPages; page++) {
const nextUrl = `https://fundf10.eastmoney.com/F10DataApi.aspx?type=lsjz&code=${code}&page=${page}&per=${per}&sdate=${sdate}&edate=${edate}`;
await loadScript(nextUrl);
if (window.apidata && window.apidata.content) {
allData = allData.concat(parseContent(window.apidata.content));
const nextApidata = await loadScript(nextUrl);
if (nextApidata && nextApidata.content) {
allData = allData.concat(parseContent(nextApidata.content));
}
}