feat: 添加收支趋势折线图组件,展示收入、支出和结余随时间变化的趋势。

This commit is contained in:
2026-01-29 07:42:34 +08:00
parent c039d869a3
commit ca2c2073fb

View File

@@ -38,18 +38,232 @@ function TrendLineChart({ data, title = '收支趋势', loading = false }: Trend
trigger: 'axis', trigger: 'axis',
confine: true, // Keep tooltip inside chart area confine: true, // Keep tooltip inside chart area
backgroundColor: 'rgba(255, 255, 255, 0.8)', backgroundColor: 'rgba(255, 255, 255, 0.8)',
// ... (keep existing properties) ... borderColor: '#e2e8f0',
borderWidth: 1,
textStyle: {
color: '#1e293b',
},
extraCssText: 'backdrop-filter: blur(8px); border-radius: 8px; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985',
},
},
formatter: (params: any) => {
let result = `<div style="font-weight: 600; margin-bottom: 8px;">${params[0].axisValue}</div>`;
let income = 0;
let expense = 0;
params.forEach((param: any) => {
const color = param.color;
const value = param.value;
if (param.seriesName === '收入') income = value;
if (param.seriesName === '支出') expense = value;
result += `<div style="display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 4px;">
<span style="display: flex; align-items: center; gap: 6px;">
<span style="display: inline-block; width: 8px; height: 8px; border-radius: 50%; background-color: ${color};"></span>
<span style="color: #64748b; font-size: 12px;">${param.seriesName}</span>
</span>
<span style="font-weight: 600; font-family: 'Inter', sans-serif;">¥${value.toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}</span>
</div>`;
});
const net = income - expense;
const isSurplus = net >= 0;
const netColor = isSurplus ? '#10b981' : '#ef4444';
const netLabel = isSurplus ? '结余' : '赤字';
result += `<div style="margin-top: 8px; padding-top: 8px; border-top: 1px dashed #e2e8f0; display: flex; align-items: center; justify-content: space-between; gap: 16px;">
<span style="color: #64748b; font-size: 12px;">本日${netLabel}</span>
<span style="font-weight: 700; color: ${netColor}; font-family: 'Inter', sans-serif;">
${isSurplus ? '+' : ''}¥${net.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
</span>
</div>`;
return result;
},
}, },
// ... dataZoom: [
return( {
<div className = "trend-line-chart" > type: 'slider',
<ReactECharts show: true,
option={option} xAxisIndex: [0],
style={{ height: '100%', width: '100%', minHeight: '300px' }} start: 0,
notMerge={true} end: 100,
lazyUpdate={true} bottom: 0,
/> borderColor: 'transparent',
</div > fillerColor: 'rgba(99, 102, 241, 0.1)',
handleStyle: {
color: '#6366f1',
},
},
{
type: 'inside',
xAxisIndex: [0],
start: 0,
end: 100,
},
],
legend: {
data: ['收入', '支出', '结余'],
top: 40,
left: 'center',
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
top: 80,
containLabel: true,
},
xAxis: {
type: 'category',
boundaryGap: false,
data: data.map((item) => formatDate(item.date)),
axisLabel: {
rotate: 45,
fontSize: 11,
},
},
yAxis: {
type: 'value',
axisLabel: {
formatter: (value: number) => {
if (value >= 10000) {
return `${(value / 10000).toFixed(1)}`;
}
return value.toFixed(0);
},
},
},
series: [
{
name: '收入',
type: 'line',
smooth: true,
data: data.map((item) => item.income),
itemStyle: {
color: '#10b981',
},
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(16, 185, 129, 0.3)' },
{ offset: 1, color: 'rgba(16, 185, 129, 0.05)' },
],
},
},
},
{
name: '支出',
type: 'line',
smooth: true,
data: data.map((item) => item.expense),
itemStyle: {
color: '#ef4444',
},
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(239, 68, 68, 0.3)' },
{ offset: 1, color: 'rgba(239, 68, 68, 0.05)' },
],
},
},
markPoint: {
data: [
{ type: 'max', name: '最大支出' },
],
symbol: 'pin',
symbolSize: 45,
itemStyle: {
color: '#ef4444'
},
label: {
color: '#fff',
fontSize: 10,
formatter: 'Max'
}
},
},
{
name: '结余',
type: 'line',
smooth: true,
showSymbol: false,
data: data.map((item) => item.balance),
itemStyle: {
color: '#3b82f6',
},
lineStyle: {
width: 2,
type: 'dashed',
},
markPoint: {
data: [
{ type: 'max', name: 'Max' },
{ type: 'min', name: 'Min' },
],
symbol: 'pin',
symbolSize: 40,
label: {
color: '#fff',
fontSize: 10,
formatter: '{c}'
}
},
markLine: {
data: [{ type: 'average', name: 'Avg' }],
precision: 0,
label: {
formatter: '均值: {c}'
}
},
},
],
};
if (loading) {
return (
<div className="trend-line-chart">
<div className="chart-loading">...</div>
</div>
);
}
if (!data || data.length === 0) {
return (
<div className="trend-line-chart">
<div className="chart-empty"></div>
</div>
);
}
return (
<div className="trend-line-chart">
<ReactECharts
option={option}
style={{ height: '100%', width: '100%', minHeight: '300px' }}
notMerge={true}
lazyUpdate={true}
/>
</div>
); );
} }