From 6eb70c93b7b7baf702ecc1d09f2b34fa80c838cd Mon Sep 17 00:00:00 2001 From: hzm <934585316@qq.com> Date: Sat, 31 Jan 2026 21:29:46 +0800 Subject: [PATCH] =?UTF-8?q?add:=20=E6=8E=A5=E5=8F=A3=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/fund/route.js | 19 +++- app/page.jsx | 201 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 204 insertions(+), 16 deletions(-) diff --git a/app/api/fund/route.js b/app/api/fund/route.js index 2c22ba7..622ca99 100644 --- a/app/api/fund/route.js +++ b/app/api/fund/route.js @@ -1,5 +1,16 @@ 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' }); @@ -75,18 +86,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 }); + return NextResponse.json({ error: '缺少基金编号' }, { status: 400, headers: CORS_HEADERS }); } const [gz, holdings] = await Promise.allSettled([fetchGZ(code), fetchHoldings(code)]); if (gz.status !== 'fulfilled') { - return NextResponse.json({ error: gz.reason?.message || '基金估值获取失败' }, { status: 404 }); + return NextResponse.json({ error: gz.reason?.message || '基金估值获取失败' }, { status: 404, headers: CORS_HEADERS }); } const data = { ...gz.value, holdings: holdings.status === 'fulfilled' ? holdings.value : [] }; - return NextResponse.json(data, { status: 200 }); + return NextResponse.json(data, { status: 200, headers: CORS_HEADERS }); } catch (e) { - return NextResponse.json({ error: e.message || '服务异常' }, { status: 500 }); + return NextResponse.json({ error: e.message || '服务异常' }, { status: 500, headers: CORS_HEADERS }); } } diff --git a/app/page.jsx b/app/page.jsx index 862d148..7612ab0 100644 --- a/app/page.jsx +++ b/app/page.jsx @@ -66,6 +66,15 @@ 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 { @@ -79,6 +88,13 @@ 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 {} }, []); @@ -93,15 +109,139 @@ 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(/
| ([\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:' |