'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)}秒
{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) => (
))}
)}
);
}