128 lines
4.1 KiB
TypeScript
128 lines
4.1 KiB
TypeScript
import React, { useState, useMemo } from 'react';
|
|
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Brush } from 'recharts';
|
|
|
|
interface StockChartProps {
|
|
data: Array<{ date: string; value: number }>;
|
|
color?: string;
|
|
showBrush?: boolean;
|
|
height?: string;
|
|
showTimeRange?: boolean; // 是否显示时间范围选择
|
|
legend?: string; // 图例文本
|
|
}
|
|
|
|
export const StockChart: React.FC<StockChartProps> = ({
|
|
data,
|
|
color = '#882323',
|
|
showBrush = true,
|
|
height = '100%',
|
|
showTimeRange = true,
|
|
legend
|
|
}) => {
|
|
const [selectedRange, setSelectedRange] = useState<string>('ALL');
|
|
|
|
// 根据选择的时间范围过滤数据
|
|
const filteredData = useMemo(() => {
|
|
if (selectedRange === 'ALL') return data;
|
|
|
|
const rangeMap: Record<string, number> = {
|
|
'1Y': 12,
|
|
'3Y': 36,
|
|
'5Y': 60
|
|
};
|
|
|
|
const months = rangeMap[selectedRange];
|
|
if (!months) return data;
|
|
|
|
return data.slice(-months);
|
|
}, [data, selectedRange]);
|
|
|
|
return (
|
|
<div className="relative w-full h-full flex flex-col">
|
|
{/* 顶部栏:图例和时间范围选择 */}
|
|
<div className="flex items-center justify-between mb-2">
|
|
{/* 图例 - 左上角 */}
|
|
{legend && (
|
|
<div className="flex items-center gap-2 text-xs text-slate-500">
|
|
<span className="w-2 h-2 rounded-full" style={{ backgroundColor: color }}></span>
|
|
<span>{legend}</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* 时间范围选择按钮 - 右上角 */}
|
|
{showTimeRange && (
|
|
<div className="flex items-center gap-1 ml-auto">
|
|
{['1Y', '3Y', '5Y', 'ALL'].map(range => (
|
|
<button
|
|
key={range}
|
|
onClick={() => setSelectedRange(range)}
|
|
className={`px-2 py-1 text-xs font-medium rounded transition-all ${
|
|
selectedRange === range
|
|
? 'bg-slate-900 text-white shadow-sm'
|
|
: 'bg-white text-slate-600 hover:bg-slate-100 border border-slate-200'
|
|
}`}
|
|
>
|
|
{range}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 图表容器 */}
|
|
<div className="flex-1 min-h-0">
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<AreaChart data={filteredData} margin={{ top: 5, right: 0, left: -20, bottom: showBrush ? 35 : 10 }}>
|
|
<defs>
|
|
<linearGradient id="brushGradient" x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="0%" stopColor="#6366f1" stopOpacity={0.15} />
|
|
<stop offset="100%" stopColor="#6366f1" stopOpacity={0.05} />
|
|
</linearGradient>
|
|
</defs>
|
|
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#f1f5f9" />
|
|
<XAxis
|
|
dataKey="date"
|
|
axisLine={false}
|
|
tickLine={false}
|
|
tick={{fontSize: 10, fill: '#cbd5e1'}}
|
|
dy={10}
|
|
interval="preserveStartEnd"
|
|
/>
|
|
<YAxis
|
|
axisLine={false}
|
|
tickLine={false}
|
|
tick={{fontSize: 10, fill: '#cbd5e1'}}
|
|
domain={['dataMin - 1', 'dataMax + 1']}
|
|
/>
|
|
<Tooltip
|
|
contentStyle={{
|
|
borderRadius: '12px',
|
|
border: 'none',
|
|
boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)',
|
|
padding: '8px 12px'
|
|
}}
|
|
itemStyle={{ color: color, fontSize: '12px', fontWeight: 600 }}
|
|
labelStyle={{ color: '#64748b', fontSize: '10px', marginBottom: '4px' }}
|
|
/>
|
|
{showBrush && (
|
|
<Brush
|
|
dataKey="date"
|
|
height={20}
|
|
stroke="#c0c8e5"
|
|
fill="url(#brushGradient)"
|
|
travellerWidth={16}
|
|
/>
|
|
)}
|
|
<Area
|
|
type="monotone"
|
|
dataKey="value"
|
|
stroke={color}
|
|
strokeWidth={2}
|
|
fill="none"
|
|
/>
|
|
</AreaChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|