stock-portfolio_byQwen3.6/src/components/dashboard/net-worth-chart.tsx

142 lines
5.6 KiB
TypeScript

'use client';
import { AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts';
interface Snapshot {
date: string;
totalValueCny: string;
totalCostCny: string;
}
interface NetWorthChartProps {
snapshots: Snapshot[];
}
function CustomTooltip({ active, payload, label }: { active?: boolean; payload?: Array<{ name: string; value: string; payload: Snapshot }>; label?: string }) {
if (active && payload && payload.length) {
const data = payload[0].payload;
const totalValue = parseFloat(data.totalValueCny);
const totalCost = parseFloat(data.totalCostCny);
const pnl = totalValue - totalCost;
const pnlPercent = totalCost > 0 ? (pnl / totalCost) * 100 : 0;
const isPositive = pnl >= 0;
const formattedDate = (label || '').replace(/-/g, '/');
return (
<div style={{
backgroundColor: 'hsl(var(--card))',
borderColor: 'hsl(var(--border))',
borderRadius: '8px',
color: 'hsl(var(--foreground))',
fontSize: '13px',
padding: '12px 16px',
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
}}>
<div style={{ fontWeight: '600', marginBottom: '8px', fontSize: '14px' }}>
{formattedDate}
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '24px', marginBottom: '4px' }}>
<span style={{ color: 'hsl(var(--muted-foreground))', display: 'flex', alignItems: 'center', gap: '6px' }}>
<span style={{ display: 'inline-block', width: '8px', height: '8px', borderRadius: '2px', backgroundColor: '#f59e0b' }} />
</span>
<span style={{ fontWeight: '700', color: '#f59e0b' }}>
¥{totalValue.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '24px', marginBottom: '4px' }}>
<span style={{ color: 'hsl(var(--muted-foreground))', display: 'flex', alignItems: 'center', gap: '6px' }}>
<span style={{ display: 'inline-block', width: '8px', height: '8px', borderRadius: '2px', backgroundColor: '#9ca3af' }} />
</span>
<span style={{ fontWeight: '600', color: 'hsl(var(--muted-foreground))' }}>
¥{totalCost.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '24px', paddingTop: '6px', borderTop: '1px solid hsl(var(--border))' }}>
<span style={{ color: 'hsl(var(--muted-foreground))', fontWeight: '500' }}></span>
<span style={{
fontWeight: '700',
color: isPositive ? '#ef4444' : '#22c55e',
}}>
{isPositive ? '+' : ''}¥{pnl.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
{' '}({isPositive ? '+' : ''}{pnlPercent.toFixed(2)}%)
</span>
</div>
</div>
);
}
return null;
}
export default function NetWorthChart({ snapshots }: NetWorthChartProps) {
if (!snapshots || snapshots.length === 0) {
return (
<div className="flex items-center justify-center h-[320px] text-muted-foreground">
</div>
);
}
const chartData = snapshots.map(s => ({
date: s.date.replace(/-/g, '/'),
totalValueCny: parseFloat(s.totalValueCny),
totalCostCny: parseFloat(s.totalCostCny),
}));
return (
<div className="h-[320px] w-full">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={chartData} margin={{ top: 10, right: 20, left: 10, bottom: 10 }}>
<defs>
<linearGradient id="totalValueGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#f59e0b" stopOpacity={0.3} />
<stop offset="100%" stopColor="#f59e0b" stopOpacity={0.02} />
</linearGradient>
<linearGradient id="totalCostGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#9ca3af" stopOpacity={0.15} />
<stop offset="100%" stopColor="#9ca3af" stopOpacity={0.02} />
</linearGradient>
</defs>
<XAxis
dataKey="date"
tick={{ fontSize: 12, fill: 'hsl(var(--muted-foreground))' }}
axisLine={{ stroke: 'hsl(var(--border))' }}
tickLine={{ stroke: 'hsl(var(--border))' }}
interval="preserveStartEnd"
/>
<YAxis
tick={{ fontSize: 12, fill: 'hsl(var(--muted-foreground))' }}
axisLine={{ stroke: 'hsl(var(--border))' }}
tickLine={{ stroke: 'hsl(var(--border))' }}
tickFormatter={(value: number) => {
if (value >= 1000000) return `¥${(value / 1000000).toFixed(1)}M`;
if (value >= 1000) return `¥${(value / 1000).toFixed(0)}K`;
return `¥${value.toFixed(0)}`;
}}
/>
<Tooltip content={<CustomTooltip />} />
<Area
type="monotone"
dataKey="totalValueCny"
stroke="#f59e0b"
strokeWidth={2}
fill="url(#totalValueGradient)"
name="总市值"
/>
<Area
type="monotone"
dataKey="totalCostCny"
stroke="#9ca3af"
strokeWidth={1.5}
strokeDasharray="4 4"
fill="url(#totalCostGradient)"
name="投入本金"
/>
</AreaChart>
</ResponsiveContainer>
</div>
);
}