diff --git a/app/api/fund/route.js b/app/api/fund/route.js index 622ca99..2c22ba7 100644 --- a/app/api/fund/route.js +++ b/app/api/fund/route.js @@ -1,16 +1,5 @@ import { NextResponse } from 'next/server'; -const CORS_HEADERS = { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET,OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type,Authorization,Accept', - 'Access-Control-Max-Age': '86400' -}; - -export async function OPTIONS() { - return new NextResponse(null, { status: 204, headers: CORS_HEADERS }); -} - async function fetchGZ(code) { const url = `https://fundgz.1234567.com.cn/js/${code}.js`; const res = await fetch(url, { cache: 'no-store' }); @@ -86,18 +75,18 @@ export async function GET(req) { const { searchParams } = new URL(req.url); const code = (searchParams.get('code') || '').trim(); if (!code) { - return NextResponse.json({ error: '缺少基金编号' }, { status: 400, headers: CORS_HEADERS }); + return NextResponse.json({ error: '缺少基金编号' }, { status: 400 }); } const [gz, holdings] = await Promise.allSettled([fetchGZ(code), fetchHoldings(code)]); if (gz.status !== 'fulfilled') { - return NextResponse.json({ error: gz.reason?.message || '基金估值获取失败' }, { status: 404, headers: CORS_HEADERS }); + return NextResponse.json({ error: gz.reason?.message || '基金估值获取失败' }, { status: 404 }); } const data = { ...gz.value, holdings: holdings.status === 'fulfilled' ? holdings.value : [] }; - return NextResponse.json(data, { status: 200, headers: CORS_HEADERS }); + return NextResponse.json(data, { status: 200 }); } catch (e) { - return NextResponse.json({ error: e.message || '服务异常' }, { status: 500, headers: CORS_HEADERS }); + return NextResponse.json({ error: e.message || '服务异常' }, { status: 500 }); } } diff --git a/app/page.jsx b/app/page.jsx index 7612ab0..862d148 100644 --- a/app/page.jsx +++ b/app/page.jsx @@ -66,15 +66,6 @@ export default function HomePage() { const [settingsOpen, setSettingsOpen] = useState(false); const [tempSeconds, setTempSeconds] = useState(30); const [manualRefreshing, setManualRefreshing] = useState(false); - const [useJsonp, setUseJsonp] = useState(false); - const [apiBase, setApiBase] = useState('/api'); - const [tempApiBase, setTempApiBase] = useState('/api'); - - const buildUrl = (path) => { - const base = (apiBase || '/api').replace(/\/$/, ''); - const p = path.startsWith('/') ? path : `/${path}`; - return `${base}${p}`; - }; useEffect(() => { try { @@ -88,13 +79,6 @@ export default function HomePage() { setRefreshMs(savedMs); setTempSeconds(Math.round(savedMs / 1000)); } - const savedApi = localStorage.getItem('apiBase'); - if (savedApi && typeof savedApi === 'string') { - setApiBase(savedApi); - setTempApiBase(savedApi); - } - const savedJsonp = localStorage.getItem('useJsonp'); - if (savedJsonp === 'true') setUseJsonp(true); } catch {} }, []); @@ -109,139 +93,15 @@ export default function HomePage() { }; }, [funds, refreshMs]); - const loadScript = (src, timeoutMs = 8000) => { - return new Promise((resolve, reject) => { - const s = document.createElement('script'); - s.src = src; - s.async = true; - let done = false; - const clear = () => { - if (s.parentNode) s.parentNode.removeChild(s); - }; - const timer = setTimeout(() => { - if (done) return; - done = true; - clear(); - reject(new Error('JSONP 加载超时')); - }, timeoutMs); - s.onload = () => { - if (done) return; - done = true; - clearTimeout(timer); - clear(); - resolve(); - }; - s.onerror = () => { - if (done) return; - done = true; - clearTimeout(timer); - clear(); - reject(new Error('JSONP 加载失败')); - }; - document.head.appendChild(s); - }); - }; - - const getFundGZByJsonp = async (c) => { - return new Promise(async (resolve, reject) => { - const prev = window.jsonpgz; - try { - window.jsonpgz = (json) => { - try { - const gszzlNum = Number(json.gszzl); - const data = { - code: json.fundcode, - name: json.name, - dwjz: json.dwjz, - gsz: json.gsz, - gztime: json.gztime, - gszzl: Number.isFinite(gszzlNum) ? gszzlNum : json.gszzl - }; - resolve(data); - } catch (e) { - reject(e); - } finally { - window.jsonpgz = prev; - } - }; - await loadScript(`https://fundgz.1234567.com.cn/js/${encodeURIComponent(c)}.js`); - // 如果脚本未触发回调 - setTimeout(() => { - reject(new Error('估值 JSONP 未返回')); - window.jsonpgz = prev; - }, 0); - } catch (e) { - window.jsonpgz = prev; - reject(e); - } - }); - }; - - const stripHtml = (s) => s.replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim(); - const parseHoldingsHtml = (html) => { - const list = []; - const tableMatch = html.match(//i); - const table = tableMatch ? tableMatch[0] : html; - const rows = table.match(//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); - }; - - const getHoldingsByJsonp = async (c) => { - return new Promise(async (resolve, reject) => { - try { - // Eastmoney 返回形如 var apidata={content:'...
'} - // 先清理可能残留的变量 - delete window.apidata; - await loadScript(`https://fundf10.eastmoney.com/FundArchivesDatas.aspx?type=jjcc&code=${encodeURIComponent(c)}&topline=10&year=&month=&rt=${Date.now()}`); - const content = window.apidata?.content || ''; - const m = content.match(//i) || content.match(/content:\s*'([\s\S]*?)'/i); - const html = m ? (m[0].startsWith(' { - if (useJsonp) { - const gz = await getFundGZByJsonp(c); - const holdings = await getHoldingsByJsonp(c); - return { ...gz, holdings }; - } - const res = await fetch(buildUrl(`/fund?code=${encodeURIComponent(c)}`), { cache: 'no-store' }); - if (!res.ok) throw new Error('网络错误'); - return await res.json(); - }; - const refreshAll = async (codes) => { try { const updated = await Promise.all( - codes.map((c) => fetchFundData(c)) + codes.map(async (c) => { + const res = await fetch(`/api/fund?code=${encodeURIComponent(c)}`, { cache: 'no-store' }); + if (!res.ok) throw new Error('网络错误'); + const data = await res.json(); + return data; + }) ); setFunds(updated); localStorage.setItem('funds', JSON.stringify(updated)); @@ -264,7 +124,9 @@ export default function HomePage() { } setLoading(true); try { - const data = await fetchFundData(clean); + const res = await fetch(`/api/fund?code=${encodeURIComponent(clean)}`, { cache: 'no-store' }); + if (!res.ok) throw new Error('基金未找到或接口异常'); + const data = await res.json(); const next = [data, ...funds]; setFunds(next); localStorage.setItem('funds', JSON.stringify(next)); @@ -299,10 +161,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); - localStorage.setItem('useJsonp', useJsonp ? 'true' : 'false'); setSettingsOpen(false); }; @@ -439,48 +297,13 @@ export default function HomePage() {
数据源:基金估值与重仓来自东方财富公开接口,可能存在延迟
{settingsOpen && ( -
setSettingsOpen(false)}> +
setSettingsOpen(false)}>
e.stopPropagation()}>
- 设置 - 数据源与刷新频率 + 刷新频率设置 + 选择预设或自定义秒数
-
- 数据源 - 跨域环境推荐 JSONP 直连 -
-
- {[ - { key: 'api', label: '后端 API(推荐)', active: !useJsonp }, - { key: 'jsonp', label: 'JSONP 直连', active: useJsonp } - ].map((opt) => ( - - ))} -
- {!useJsonp && ( -
- - setTempApiBase(e.target.value)} - placeholder="/api 或 https://你的域名/api" - /> -
- )}
{[10, 30, 60, 120, 300].map((s) => (