feat: 添加收支趋势折线图组件,展示收入、支出和结余随时间变化的趋势。
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user