'use client'; import { useEffect, useRef, useState } from 'react'; function PlusIcon(props) { return ( ); } function TrashIcon(props) { return ( ); } function SettingsIcon(props) { return ( ); } function RefreshIcon(props) { return ( ); } function Stat({ label, value, delta }) { const dir = delta > 0 ? 'up' : delta < 0 ? 'down' : ''; return (
{label} {value} {typeof delta === 'number' && ( {delta > 0 ? '↗' : delta < 0 ? '↘' : '—'} {Math.abs(delta).toFixed(2)}% )}
); } export default function HomePage() { const [funds, setFunds] = useState([]); const [code, setCode] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const timerRef = useRef(null); const [refreshMs, setRefreshMs] = useState(30000); const [settingsOpen, setSettingsOpen] = useState(false); const [tempSeconds, setTempSeconds] = useState(30); const [manualRefreshing, setManualRefreshing] = useState(false); useEffect(() => { try { const saved = JSON.parse(localStorage.getItem('funds') || '[]'); if (Array.isArray(saved) && saved.length) { setFunds(saved); refreshAll(saved.map((f) => f.code)); } const savedMs = parseInt(localStorage.getItem('refreshMs') || '30000', 10); if (Number.isFinite(savedMs) && savedMs > 0) { setRefreshMs(savedMs); setTempSeconds(Math.round(savedMs / 1000)); } } catch {} }, []); useEffect(() => { if (timerRef.current) clearInterval(timerRef.current); timerRef.current = setInterval(() => { const codes = funds.map((f) => f.code); if (codes.length) refreshAll(codes); }, refreshMs); return () => { if (timerRef.current) clearInterval(timerRef.current); }; }, [funds, refreshMs]); const refreshAll = async (codes) => { try { const updated = await Promise.all( 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)); } catch (e) { console.error(e); } }; const addFund = async (e) => { e.preventDefault(); setError(''); const clean = code.trim(); if (!clean) { setError('请输入基金编号'); return; } if (funds.some((f) => f.code === clean)) { setError('该基金已添加'); return; } setLoading(true); try { 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)); setCode(''); } catch (e) { setError(e.message || '添加失败'); } finally { setLoading(false); } }; const removeFund = (removeCode) => { const next = funds.filter((f) => f.code !== removeCode); setFunds(next); localStorage.setItem('funds', JSON.stringify(next)); }; const manualRefresh = async () => { if (manualRefreshing) return; const codes = funds.map((f) => f.code); if (!codes.length) return; setManualRefreshing(true); try { await refreshAll(codes); } finally { setManualRefreshing(false); } }; const saveSettings = (e) => { e?.preventDefault?.(); const ms = Math.max(5, Number(tempSeconds)) * 1000; setRefreshMs(ms); localStorage.setItem('refreshMs', String(ms)); setSettingsOpen(false); }; useEffect(() => { const onKey = (ev) => { if (ev.key === 'Escape' && settingsOpen) setSettingsOpen(false); }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [settingsOpen]); return (
实时基金估值
刷新 {Math.round(refreshMs / 1000)}秒
添加基金 输入基金编号(例如:110022)
setCode(e.target.value)} inputMode="numeric" aria-invalid={!!error} />
{error &&
{error}
}
{funds.length === 0 ? (
尚未添加基金
) : (
{funds.map((f) => (
{f.name} #{f.code}
估值时间 {f.gztime || f.time || '-'}
前10重仓股票 持仓占比
{Array.isArray(f.holdings) && f.holdings.length ? (
{f.holdings.map((h, idx) => (
{h.name ? h.name : h.code} {h.code ? ` (${h.code})` : ''} {h.weight}
))}
) : (
暂无重仓数据
)}
))}
)}
数据源:基金估值与重仓来自东方财富公开接口,可能存在延迟
{settingsOpen && (
setSettingsOpen(false)}>
e.stopPropagation()}>
刷新频率设置 选择预设或自定义秒数
{[10, 30, 60, 120, 300].map((s) => ( ))}
setTempSeconds(Number(e.target.value))} placeholder="秒数(≥5)" />
)}
); }