You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

160 lines
5.3 KiB

import { useEffect, useRef, useState } from 'react';
import { TrendingUp, TrendingDown, Activity, DollarSign, BarChart3, Zap } from 'lucide-react';
import type { MarketOverview } from '@/types';
interface MarketOverviewProps {
data: MarketOverview;
}
function AnimatedNumber({ value, decimals = 1, suffix = '' }: { value: number; decimals?: number; suffix?: string }) {
const [displayValue, setDisplayValue] = useState(0);
const startTime = useRef<number | null>(null);
const duration = 1000;
useEffect(() => {
const animate = (timestamp: number) => {
if (!startTime.current) startTime.current = timestamp;
const progress = Math.min((timestamp - startTime.current) / duration, 1);
const easeOut = 1 - Math.pow(1 - progress, 3);
setDisplayValue(value * easeOut);
if (progress < 1) {
requestAnimationFrame(animate);
}
};
requestAnimationFrame(animate);
return () => {
startTime.current = null;
};
}, [value]);
return (
<span>
{displayValue.toFixed(decimals)}{suffix}
</span>
);
}
function MiniChart({ isUp }: { isUp: boolean }) {
const points = isUp
? '0,30 10,25 20,28 30,20 40,22 50,15 60,18 70,10 80,12 90,5 100,8'
: '0,10 10,15 20,12 30,20 40,18 50,25 60,22 70,30 80,28 90,35 100,32';
return (
<svg viewBox="0 0 100 40" className="w-full h-10">
<polyline
fill="none"
stroke={isUp ? '#0ECB81' : '#F6465D'}
strokeWidth="2"
points={points}
/>
<defs>
<linearGradient id={`gradient-${isUp}`} x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stopColor={isUp ? '#0ECB81' : '#F6465D'} stopOpacity="0.3" />
<stop offset="100%" stopColor={isUp ? '#0ECB81' : '#F6465D'} stopOpacity="0" />
</linearGradient>
</defs>
<polygon
fill={`url(#gradient-${isUp})`}
points={`${points} 100,40 0,40`}
/>
</svg>
);
}
export function MarketOverviewPanel({ data }: MarketOverviewProps) {
const cards = [
{
title: '市场热度指数',
value: data.heatIndex,
change: data.heatChange,
suffix: '',
decimals: 1,
icon: Activity,
isUp: data.heatChange > 0,
description: data.heatChange > 0 ? '多头情绪高涨' : '市场情绪偏冷',
},
{
title: '涨跌分布',
value: data.upCount,
change: data.downCount,
suffix: '',
decimals: 0,
icon: BarChart3,
isUp: true,
description: `涨: ${data.upCount} | 跌: ${data.downCount}`,
isDistribution: true,
},
{
title: '资金流向',
value: data.capitalFlow,
change: 0,
suffix: '亿',
decimals: 1,
icon: DollarSign,
isUp: data.capitalFlow > 0,
description: data.capitalFlow > 0 ? '资金净流入' : '资金净流出',
},
{
title: '波动率指数',
value: data.volatilityIndex,
change: data.volatilityChange,
suffix: '',
decimals: 1,
icon: Zap,
isUp: data.volatilityChange > 0,
description: data.volatilityChange > 0 ? '波动扩大' : '波动收窄',
},
];
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-space-6">
{cards.map((card, index) => (
<div
key={card.title}
className="bg-white border border-binanceBorder-light rounded-card p-space-5 hover:shadow-card-hover transition-all duration-300 group"
style={{
animationDelay: `${index * 100}ms`,
}}
>
<div className="flex items-start justify-between mb-space-3">
<div className="flex items-center gap-2">
<div className={`p-2 rounded-lg ${card.isUp ? 'bg-semantic-green/10' : 'bg-semantic-red/10'}`}>
<card.icon className={`w-4 h-4 ${card.isUp ? 'text-semantic-green' : 'text-semantic-red'}`} />
</div>
<span className="text-text-slate text-sm">{card.title}</span>
</div>
{!card.isDistribution && card.change !== 0 && (
<div className={`flex items-center gap-1 text-xs ${card.change > 0 ? 'text-semantic-green' : 'text-semantic-red'}`}>
{card.change > 0 ? <TrendingUp className="w-3 h-3" /> : <TrendingDown className="w-3 h-3" />}
<span>{card.change > 0 ? '+' : ''}{card.change}%</span>
</div>
)}
</div>
<div className="mb-space-3">
<span className={`text-2xl font-bold font-mono ${card.isUp ? 'text-text-ink' : 'text-semantic-red'}`}>
{card.isDistribution ? (
<span className="flex items-baseline gap-2">
<span className="text-semantic-green">{data.upCount}</span>
<span className="text-text-slate text-lg">/</span>
<span className="text-semantic-red">{data.downCount}</span>
</span>
) : (
<>
<AnimatedNumber value={card.value} decimals={card.decimals} suffix={card.suffix} />
</>
)}
</span>
</div>
<div className="text-xs text-text-slate mb-space-3">{card.description}</div>
<MiniChart isUp={card.isUp} />
</div>
))}
</div>
);
}