import { useEffect, useState } from 'react' import { BarChart3, TrendingUp, TrendingDown, Minus, RefreshCw, Loader2, AlertCircle, Calendar, Activity, } from 'lucide-react' import { api, MarketOverview, SyncJob, StockRow } from '@/lib/api' import { canManageUsers } from '@/lib/auth' import { cn } from '@/lib/cn' interface StockOverviewProps { role: 'system_admin' | 'admin' | 'user' } export function StockOverview({ role }: StockOverviewProps) { const [overview, setOverview] = useState(null) const [job, setJob] = useState(null) const [loading, setLoading] = useState(true) const [syncing, setSyncing] = useState(false) const [error, setError] = useState('') const canSync = canManageUsers(role) const fetchAll = async () => { setLoading(true) setError('') try { const [o, j] = await Promise.all([api.stockOverview(), api.stockSyncStatus()]) setOverview(o) setJob(j) } catch (err: any) { setError(err?.message || '加载市场数据失败') } finally { setLoading(false) } } useEffect(() => { fetchAll() }, []) const handleSync = async () => { setSyncing(true) setError('') try { const res = await api.triggerStockSync() await fetchAll() // eslint-disable-next-line no-console console.log('synced', res.records_count, 'records') } catch (err: any) { setError(err?.message || '同步失败') } finally { setSyncing(false) } } if (loading) { return (
加载市场数据…
) } if (!overview?.latest_trade_date) { return (
暂无市场数据
{canSync && ( )}
) } const { counts, avg_change_pct, indices, top_gainers, top_losers, turnover_leaders } = overview return (

市场概览

{overview.latest_trade_date}
{job && ( 上次同步:{formatSyncStatus(job)} )} {canSync && ( )}
{error && (
{error}
)}
} label="上涨" value={counts.up} /> } label="下跌" value={counts.down} /> } label="平盘" value={counts.flat} /> } label="平均涨跌" value={`${avg_change_pct >= 0 ? '+' : ''}${avg_change_pct.toFixed(2)}%`} valueClass={avg_change_pct >= 0 ? 'text-bull' : 'text-bear'} />
{indices.length > 0 && (
{indices.map((item) => ( ))}
)}
) } function KpiCard({ icon, label, value, valueClass, }: { icon: React.ReactNode label: string value: React.ReactNode valueClass?: string }) { return (
{icon}
{value}
{label}
) } function IndexCard({ item }: { item: StockRow }) { const positive = item.change_pct >= 0 return (
{item.name || item.symbol}
{item.close.toFixed(2)}
{positive ? '+' : ''}{item.change_pct.toFixed(2)}%
) } function StockList({ title, rows, valueKey, }: { title: string rows: StockRow[] valueKey: 'change_pct' | 'amount' }) { return (

{title}

{rows.length === 0 ? (
暂无数据
) : ( rows.map((item, idx) => { const val = valueKey === 'amount' ? (item.amount ?? 0) / 1e8 : item.change_pct const isPositive = valueKey === 'amount' ? true : item.change_pct >= 0 return (
{idx + 1} {item.name || item.symbol}
{valueKey === 'amount' ? `${val.toFixed(2)}亿` : `${isPositive ? '+' : ''}${val.toFixed(2)}%`}
) }) )}
) } function formatSyncStatus(job: SyncJob): string { if (job.status === 'running') return '同步中…' const timeStr = job.finished_at ? new Date(job.finished_at).toLocaleString() : new Date(job.started_at).toLocaleString() const statusText = job.status === 'success' ? '成功' : '失败' return `${statusText} · ${timeStr}` }