Files
gerenjizhang/prototype/statistics.html
T

1432 lines
52 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>统计报表 - 个人收支管理系统</title>
<!-- ECharts CDN -->
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<!-- 公共样式 -->
<link rel="stylesheet" href="./css/style.css">
<style>
/* ============================================
统计页面专用样式
============================================ */
/* 顶部操作栏 */
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 24px;
background: var(--surface);
border-radius: var(--radius-lg);
margin-bottom: 24px;
box-shadow: var(--shadow-sm);
}
.header-left {
display: flex;
align-items: center;
gap: 16px;
}
/* 返回按钮 - 桌面端显示,移动端隐藏 */
.back-btn {
width: 40px;
height: 40px;
border-radius: var(--radius-full);
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: 1px solid var(--border);
cursor: pointer;
font-size: 18px;
color: var(--text-primary);
transition: all 0.2s ease;
}
.back-btn:hover {
background: rgba(0, 82, 255, 0.08);
border-color: var(--primary);
}
.back-btn svg {
width: 20px;
height: 20px;
}
.page-title {
font-size: 20px;
font-weight: 600;
color: var(--text-primary);
margin: 0;
}
.header-right {
display: flex;
align-items: center;
gap: 12px;
}
/* Primary按钮 - 导出按钮 #0052ff */
.btn-primary {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 8px 18px;
min-height: 40px;
background: var(--primary);
color: #ffffff;
border: none;
border-radius: var(--radius-md);
cursor: pointer;
font-size: 14px;
font-weight: 500;
line-height: 1.4;
white-space: nowrap;
transition: all 0.2s ease;
}
.btn-primary:hover {
background: var(--primary-hover);
}
.btn-primary svg {
width: 16px;
height: 16px;
flex-shrink: 0;
}
/* Ghost按钮 - 编辑按钮 */
.btn-ghost {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 8px 16px;
min-height: 40px;
background: transparent;
color: var(--primary);
border: none;
border-radius: var(--radius-md);
cursor: pointer;
font-size: 14px;
font-weight: 500;
line-height: 1.4;
white-space: nowrap;
transition: all 0.2s ease;
}
.btn-ghost:hover {
background: rgba(0, 82, 255, 0.08);
}
.btn-ghost svg {
width: 16px;
height: 16px;
flex-shrink: 0;
}
/* 月/年切换标签 */
.period-tabs {
display: inline-flex;
align-items: center;
background: var(--bg);
border-radius: var(--radius-md);
padding: 3px;
gap: 0;
}
.period-tab {
padding: 6px 16px;
border: none;
background: transparent;
cursor: pointer;
font-size: 14px;
color: var(--text-secondary);
border-radius: var(--radius-sm);
transition: all 0.2s ease;
font-weight: 500;
line-height: 1.4;
white-space: nowrap;
min-height: 32px;
}
.period-tab:hover {
color: var(--text-primary);
}
.period-tab.active {
background: var(--surface);
color: var(--primary);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
font-weight: 600;
}
/* 内容区域 */
.statistics-content {
padding: 24px;
max-width: 1280px;
}
/* 图表类型切换 - 选中态: 背景#0052ff,文字白色 */
.chart-type-switch {
display: flex;
gap: 12px;
margin-bottom: 24px;
background: var(--surface);
padding: 16px;
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
}
.chart-type-btn {
flex: 1;
padding: 12px 16px;
border: 1.5px solid var(--border);
border-radius: var(--radius-md);
background: var(--surface);
cursor: pointer;
text-align: center;
font-weight: 500;
color: var(--text-secondary);
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
font-size: 14px;
line-height: 1.4;
white-space: nowrap;
min-height: 44px;
}
/* 选中态:背景#0052ff,文字白色 */
.chart-type-btn.active {
background: var(--primary);
border-color: var(--primary);
color: #ffffff;
font-weight: 600;
}
.chart-type-btn:not(.active):hover {
border-color: var(--primary);
color: var(--primary);
background: rgba(0, 82, 255, 0.04);
}
.chart-type-btn svg {
width: 18px;
height: 18px;
flex-shrink: 0;
}
/* 图表卡片 */
.chart-card {
background: var(--surface);
border-radius: var(--radius-lg);
padding: 24px;
margin-bottom: 24px;
box-shadow: var(--shadow-sm);
border: 1px solid var(--border);
}
.chart-card:hover {
box-shadow: var(--shadow-md);
}
/* 图表卡片标题 - 遵循Ant Design规范 */
.chart-card-title {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
margin: 0 0 16px 0;
padding-left: 12px;
line-height: 1.5;
position: relative;
}
/* 标题左侧装饰条 */
.chart-card-title::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 16px;
background: var(--primary);
border-radius: 2px;
}
/* ECharts容器 */
.echarts-container {
width: 100%;
height: 320px;
}
/* 饼图特殊布局 */
.pie-chart-wrapper {
display: flex;
align-items: center;
gap: 0;
justify-content: center;
}
.pie-chart-container {
flex: 1;
max-width: 400px;
height: 280px;
}
.pie-legend {
width: 160px;
display: flex;
flex-direction: column;
gap: 16px;
margin-left: -30px;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
}
.legend-color {
width: 12px;
height: 12px;
border-radius: var(--radius-sm);
flex-shrink: 0;
}
.legend-label {
color: var(--text-secondary);
flex: 1;
}
.legend-value {
font-weight: 600;
color: var(--text-primary);
}
/* Toast提示 */
.toast {
position: fixed;
top: 80px;
left: 50%;
transform: translateX(-50%) translateY(-100px);
background: var(--text-primary);
color: white;
padding: var(--space-md) var(--space-lg);
border-radius: var(--radius-md);
font-size: 14px;
font-weight: 500;
z-index: 1000;
opacity: 0;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: var(--space-sm);
}
.toast.show {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
.toast svg {
width: 18px;
height: 18px;
color: var(--success);
}
/* ============================================
移动端响应式 - 底部64px Tab导航
============================================ */
@media (max-width: 768px) {
/* 隐藏桌面侧边栏 */
.sidebar {
display: none;
}
.main-content {
margin-left: 0;
}
/* 返回按钮移动端隐藏 */
.back-btn {
display: none;
}
.page-header {
padding: 12px 16px;
margin-bottom: 16px;
}
.page-title {
font-size: 18px;
}
.header-right {
gap: 8px;
flex-wrap: wrap;
}
.btn-primary span,
.btn-ghost span {
display: none;
}
.btn-primary,
.btn-ghost {
padding: 8px 10px;
min-height: 36px;
}
.period-tabs {
padding: 2px;
}
.period-tab {
padding: 6px 12px;
font-size: 13px;
}
.statistics-content {
padding: 16px;
padding-bottom: 120px;
}
.chart-card:last-child {
margin-bottom: 40px;
}
.chart-type-switch {
padding: 12px;
gap: 8px;
flex-direction: row;
}
.chart-type-btn {
padding: 10px 8px;
font-size: 13px;
min-height: 40px;
gap: 6px;
}
.chart-type-btn svg {
width: 16px;
height: 16px;
}
.chart-card {
padding: 16px;
margin-bottom: 16px;
}
.chart-card-title {
font-size: 15px;
margin-bottom: 12px;
}
.echarts-container {
height: 260px;
}
.pie-chart-wrapper {
flex-direction: column;
gap: 16px;
}
.pie-legend {
width: 100%;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
gap: 8px 16px;
}
.legend-item {
font-size: 12px;
}
}
/* 平板适配 */
@media (min-width: 769px) and (max-width: 1023px) {
.sidebar {
width: 200px;
}
.main-content {
margin-left: 200px;
}
}
/* ============================================
底部Tab导航 - 移动端显示
============================================ */
.bottom-tab-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 64px;
background: var(--surface);
border-top: 1px solid var(--border);
display: flex;
justify-content: space-around;
align-items: center;
padding: 0;
z-index: 100;
}
.tab-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
flex: 1;
height: 100%;
color: var(--text-secondary);
text-decoration: none;
font-size: 12px;
cursor: pointer;
transition: color 0.2s ease;
}
.tab-item svg {
width: 24px;
height: 24px;
}
.tab-item.active {
color: var(--primary);
}
.tab-item.active svg {
color: var(--primary);
}
/* 桌面端隐藏底部导航 */
@media (min-width: 769px) {
.bottom-tab-bar {
display: none;
}
.statistics-content {
padding-bottom: var(--space-lg);
}
}
/* 页面缩放自适应 - 支持150%和200% */
@media (min-width: 1400px) {
.statistics-content {
max-width: 1100px;
}
}
@media (min-width: 1600px) {
.statistics-content {
max-width: 1000px;
}
}
</style>
</head>
<body>
<div class="app-layout">
<aside class="sidebar" role="navigation" aria-label="侧边导航">
<div class="sidebar-logo">
<div class="sidebar-logo-content">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="24" height="24">
<path d="M21 12V7H5a2 2 0 0 1 0-4h14v4"/>
<path d="M3 5v14a2 2 0 0 0 2 2h16v-5"/>
<path d="M18 12a2 2 0 0 0 0 4h4v-4h-4z"/>
</svg>
<span>个人记账</span>
</div>
<!-- 折叠按钮 -->
<div class="sidebar-toggle-wrapper" data-tooltip="收起侧边栏">
<button class="sidebar-toggle" type="button" aria-label="折叠/展开侧边栏">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="15 18 9 12 15 6"/>
</svg>
</button>
</div>
</div>
<nav class="sidebar-nav">
<a href="index.html" class="sidebar-nav-item" aria-label="首页">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
<polyline points="9 22 9 12 15 12 15 22"/>
</svg>
<span>首页</span>
</a>
<a href="record.html" class="sidebar-nav-item" aria-label="记账">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>
</svg>
<span>记账</span>
</a>
<a href="budget.html" class="sidebar-nav-item" aria-label="预算">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/>
<line x1="8" y1="21" x2="16" y2="21"/>
<line x1="12" y1="17" x2="12" y2="21"/>
</svg>
<span>预算</span>
</a>
<a href="statistics.html" class="sidebar-nav-item active" aria-current="page" aria-label="统计">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="20" x2="18" y2="10"/>
<line x1="12" y1="20" x2="12" y2="4"/>
<line x1="6" y1="20" x2="6" y2="14"/>
</svg>
<span>统计</span>
</a>
</nav>
</aside>
<!-- 主内容区域 -->
<main class="main-content">
<!-- 顶部操作栏 -->
<header class="page-header">
<div class="header-left">
<button class="back-btn" onclick="history.back()" aria-label="返回上一页">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M19 12H5M12 19l-7-7 7-7"/>
</svg>
</button>
<h1 class="page-title">统计报表</h1>
</div>
<div class="header-right">
<!-- Primary按钮 - 导出按钮 #0052ff -->
<button class="btn-primary" id="exportBtn" aria-label="导出报表">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7 10 12 15 17 10"/>
<line x1="12" y1="15" x2="12" y2="3"/>
</svg>
<span>导出</span>
</button>
<div class="period-tabs">
<button class="period-tab active" data-period="month"></button>
<button class="period-tab" data-period="year"></button>
</div>
</div>
</header>
<!-- 统计内容区域 -->
<div class="statistics-content">
<!-- 图表类型切换 - 选中态: 背景#0052ff,文字白色 -->
<div class="chart-type-switch" role="tablist" aria-label="图表类型">
<button class="chart-type-btn active" data-type="line" role="tab" aria-selected="true" aria-label="折线图">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 3v18h18"/>
<path d="M18 9l-5 5-4-4-3 3"/>
</svg>
折线图
</button>
<button class="chart-type-btn" data-type="bar" role="tab" aria-selected="false" aria-label="柱状图">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="12" width="4" height="9"/>
<rect x="10" y="8" width="4" height="13"/>
<rect x="17" y="4" width="4" height="17"/>
</svg>
柱状图
</button>
<button class="chart-type-btn" data-type="pie" role="tab" aria-selected="false" aria-label="饼图">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21.21 15.89A10 10 0 1 1 8 2.83"/>
<path d="M22 12A10 10 0 0 0 12 2v10z"/>
</svg>
饼图
</button>
</div>
<!-- 月度收支趋势图 -->
<section class="chart-card" data-visible="line,bar" aria-labelledby="trend-title">
<h2 class="chart-card-title" id="trend-title">月度收支趋势</h2>
<div id="trendChart" class="echarts-container" role="img" aria-label="月度收支趋势图表"></div>
</section>
<!-- 支出构成饼图 -->
<section class="chart-card" data-visible="pie" aria-labelledby="expense-title">
<h2 class="chart-card-title" id="expense-title">支出构成</h2>
<div class="pie-chart-wrapper">
<div id="expensePieChart" class="pie-chart-container" role="img" aria-label="支出构成饼图"></div>
<div class="pie-legend" id="pieLegend" aria-label="图例说明"></div>
</div>
</section>
<!-- 月度对比柱状图 -->
<section class="chart-card" data-visible="line,bar,pie" aria-labelledby="compare-title">
<h2 class="chart-card-title" id="compare-title">月度对比</h2>
<div id="compareChart" class="echarts-container" role="img" aria-label="月度对比图表"></div>
</section>
</div>
</main>
</div>
<!-- Toast提示 -->
<div class="toast" id="toast" role="alert" aria-live="polite">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
<polyline points="22 4 12 14.01 9 11.01"/>
</svg>
<span id="toastMessage">导出成功</span>
</div>
<!-- 移动端底部Tab导航 -->
<nav class="bottom-tab-bar" role="navigation" aria-label="底部导航">
<a href="index.html" class="tab-item">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
<polyline points="9 22 9 12 15 12 15 22"/>
</svg>
<span>首页</span>
</a>
<a href="record.html" class="tab-item">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>
</svg>
<span>记账</span>
</a>
<a href="budget.html" class="tab-item">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/>
<line x1="8" y1="21" x2="16" y2="21"/>
<line x1="12" y1="17" x2="12" y2="21"/>
</svg>
<span>预算</span>
</a>
<a href="statistics.html" class="tab-item active" aria-current="page">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="20" x2="18" y2="10"/>
<line x1="12" y1="20" x2="12" y2="4"/>
<line x1="6" y1="20" x2="6" y2="14"/>
</svg>
<span>统计</span>
</a>
</nav>
<script>
/**
* 统计页面JavaScript
* 功能:图表渲染、交互切换、数据展示
* 遵循规范:收入色#00c853,支出色#ff3d00
*/
// 示例数据:4月1日-25日每日收支
const dailyData = {
dates: [],
income: [],
expense: []
};
// 生成4月1日-25日的数据
for (let day = 1; day <= 25; day++) {
dailyData.dates.push(`4月${day}`);
// 收入:800-2000之间波动,整体有上升趋势
dailyData.income.push(Math.floor(800 + Math.random() * 1200 + day * 30));
// 支出:500-1500之间波动
dailyData.expense.push(Math.floor(500 + Math.random() * 1000 + day * 15));
}
// 支出类别数据 - 按照示例数据比例
// 餐饮35%、交通20%、购物25%、娱乐10%、其他10%
// 总支出:¥10,000
const expenseCategoryData = [
{ name: '餐饮', value: 3500, percentage: 35, color: '#00c853' }, // 绿色
{ name: '交通', value: 2000, percentage: 20, color: '#3B82F6' }, // 蓝色
{ name: '购物', value: 2500, percentage: 25, color: '#ff3d00' }, // 红色
{ name: '娱乐', value: 1000, percentage: 10, color: '#F59E0B' }, // 橙色
{ name: '其他', value: 1000, percentage: 10, color: '#8B5CF6' } // 紫色
];
// 月度对比数据
const compareData = {
months: ['本月', '上月'],
income: [28000, 25000],
expense: [18000, 16000],
surplus: [10000, 9000] // 盈余
};
// ECharts实例
let trendChart, expensePieChart, compareChart;
// 当前状态
let currentTrendType = 'line'; // line | bar
let currentPeriod = 'month'; // month | year
/**
* 初始化月度收支趋势图
*/
function initTrendChart() {
const chartDom = document.getElementById('trendChart');
trendChart = echarts.init(chartDom);
renderTrendChart();
}
/**
* 渲染月度收支趋势图
* 收入线:绿色 #00c853
* 支出线:红色 #ff3d00
*/
function renderTrendChart() {
let option;
if (currentTrendType === 'line') {
// 折线图配置
option = {
animation: true,
animationDuration: 1500,
animationEasing: 'cubicOut',
animationDurationUpdate: 800,
universalTransition: {
enabled: true,
seriesKey: 'trend'
},
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(26, 26, 26, 0.95)',
borderColor: 'transparent',
textStyle: { color: '#fff' },
formatter: function(params) {
let result = params[0].axisValue + '<br/>';
params.forEach(param => {
const color = param.seriesName === '收入' ? '#00c853' : '#ff3d00';
const prefix = param.seriesName === '收入' ? '+' : '-';
result += `<span style="color:${color}">●</span> ${param.seriesName}: ${prefix}¥${param.value.toLocaleString()}<br/>`;
});
return result;
},
transitionDuration: 0.3
},
legend: {
data: ['收入', '支出'],
bottom: 0,
textStyle: { color: '#737373' },
animationDurationUpdate: 300
},
grid: {
left: '3%',
right: '4%',
bottom: '15%',
top: '10%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: dailyData.dates,
axisLine: { lineStyle: { color: '#e5e5e5' } },
axisLabel: {
color: '#737373',
interval: 4,
fontSize: 11
}
},
yAxis: {
type: 'value',
axisLine: { show: false },
splitLine: { lineStyle: { color: '#f5f5f5' } },
axisLabel: {
color: '#737373',
formatter: function(value) {
return '¥' + (value / 1000) + 'k';
}
}
},
series: [
{
name: '收入',
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 6,
lineStyle: { color: '#00c853', width: 2 },
itemStyle: { color: '#00c853' },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(0, 200, 83, 0.3)' },
{ offset: 1, color: 'rgba(0, 200, 83, 0.05)' }
])
},
data: dailyData.income,
valueAnimation: true,
animationDuration: 2000,
animationEasing: 'cubicOut',
animationDelay: function(idx) {
return idx * 60;
},
emphasis: {
disabled: false,
lineStyle: {
width: 4,
shadowBlur: 10,
shadowColor: 'rgba(0, 200, 83, 0.5)'
},
itemStyle: {
shadowBlur: 10,
shadowColor: 'rgba(0, 200, 83, 0.5)',
borderColor: '#fff',
borderWidth: 2
},
scale: true,
scaleSize: 12
}
},
{
name: '支出',
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 6,
lineStyle: { color: '#ff3d00', width: 2 },
itemStyle: { color: '#ff3d00' },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(255, 61, 0, 0.3)' },
{ offset: 1, color: 'rgba(255, 61, 0, 0.05)' }
])
},
data: dailyData.expense,
valueAnimation: true,
animationDuration: 2000,
animationEasing: 'cubicOut',
animationDelay: function(idx) {
return idx * 60 + 150;
},
emphasis: {
disabled: false,
lineStyle: {
width: 4,
shadowBlur: 10,
shadowColor: 'rgba(255, 61, 0, 0.5)'
},
itemStyle: {
shadowBlur: 10,
shadowColor: 'rgba(255, 61, 0, 0.5)',
borderColor: '#fff',
borderWidth: 2
},
scale: true,
scaleSize: 12
}
}
]
};
} else {
// 柱状图配置
option = {
animation: true,
animationDuration: 1200,
animationEasing: 'elasticOut',
animationDurationUpdate: 800,
universalTransition: {
enabled: true,
seriesKey: 'trend'
},
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(26, 26, 26, 0.95)',
borderColor: 'transparent',
textStyle: { color: '#fff' },
axisPointer: { type: 'shadow' },
transitionDuration: 0.3
},
legend: {
data: ['收入', '支出'],
bottom: 0,
textStyle: { color: '#737373' },
animationDurationUpdate: 300
},
grid: {
left: '3%',
right: '4%',
bottom: '15%',
top: '10%',
containLabel: true
},
xAxis: {
type: 'category',
data: dailyData.dates,
axisLine: { lineStyle: { color: '#e5e5e5' } },
axisLabel: {
color: '#737373',
interval: 4,
fontSize: 11
}
},
yAxis: {
type: 'value',
axisLine: { show: false },
splitLine: { lineStyle: { color: '#f5f5f5' } },
axisLabel: {
color: '#737373',
formatter: function(value) {
return '¥' + (value / 1000) + 'k';
}
}
},
series: [
{
name: '收入',
type: 'bar',
barWidth: '35%',
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#00c853' },
{ offset: 1, color: '#00a844' }
]),
borderRadius: [4, 4, 0, 0]
},
data: dailyData.income,
valueAnimation: true,
animationDuration: 1500,
animationEasing: 'cubicOut',
animationDelay: function(idx) {
return idx * 50;
},
emphasis: {
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#00e05c' },
{ offset: 1, color: '#00c853' }
]),
shadowBlur: 15,
shadowColor: 'rgba(0, 200, 83, 0.4)'
},
scale: true,
scaleSize: 10
}
},
{
name: '支出',
type: 'bar',
barWidth: '35%',
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#ff3d00' },
{ offset: 1, color: '#e63600' }
]),
borderRadius: [4, 4, 0, 0]
},
data: dailyData.expense,
valueAnimation: true,
animationDuration: 1500,
animationEasing: 'cubicOut',
animationDelay: function(idx) {
return idx * 50 + 100;
},
emphasis: {
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#ff5722' },
{ offset: 1, color: '#ff3d00' }
]),
shadowBlur: 15,
shadowColor: 'rgba(255, 61, 0, 0.4)'
},
scale: true,
scaleSize: 10
}
}
]
};
}
trendChart.setOption(option, true);
}
/**
* 初始化支出构成饼图
* 环形图样式,中心显示总支出金额
*/
function initExpensePieChart() {
const chartDom = document.getElementById('expensePieChart');
expensePieChart = echarts.init(chartDom);
renderExpensePieChart();
renderPieLegend();
}
/**
* 渲染支出构成饼图 - 环形图样式
*/
function renderExpensePieChart() {
const total = expenseCategoryData.reduce((sum, item) => sum + item.value, 0);
const option = {
animation: true,
animationDuration: 1000,
animationEasing: 'cubicOut',
animationDurationUpdate: 600,
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(26, 26, 26, 0.95)',
borderColor: 'transparent',
textStyle: { color: '#fff' },
formatter: function(params) {
return `${params.name}: ¥${params.value.toLocaleString()}<br/>占比: ${params.percent}%`;
},
transitionDuration: 0.3
},
series: [
{
type: 'pie',
radius: ['45%', '75%'],
center: ['50%', '50%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 4,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: true,
position: 'center',
formatter: function() {
return '总支出\n¥' + total.toLocaleString();
},
fontSize: 16,
fontWeight: 600,
color: '#1a1a1a',
lineHeight: 24
},
emphasis: {
label: {
show: true,
fontSize: 16,
fontWeight: 600
},
scale: true,
scaleSize: 8,
itemStyle: {
shadowBlur: 20,
shadowColor: 'rgba(0, 0, 0, 0.3)'
}
},
labelLine: {
show: false
},
data: expenseCategoryData.map(item => ({
value: item.value,
name: item.name,
itemStyle: { color: item.color }
})),
animationType: 'expansion',
animationDelay: function(idx) {
return idx * 150;
}
}
]
};
expensePieChart.setOption(option);
}
/**
* 渲染饼图图例
*/
function renderPieLegend() {
const legendContainer = document.getElementById('pieLegend');
legendContainer.innerHTML = expenseCategoryData.map(item => {
return `
<div class="legend-item">
<span class="legend-color" style="background-color: ${item.color}"></span>
<span class="legend-label">${item.name}</span>
<span class="legend-value">${item.percentage}%</span>
</div>
`;
}).join('');
}
/**
* 初始化月度对比柱状图
*/
function initCompareChart() {
const chartDom = document.getElementById('compareChart');
compareChart = echarts.init(chartDom);
renderCompareChart();
}
/**
* 渲染月度对比柱状图
*/
function renderCompareChart() {
const option = {
animation: true,
animationDuration: 900,
animationEasing: 'elasticOut',
animationDurationUpdate: 500,
universalTransition: {
enabled: true
},
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(26, 26, 26, 0.95)',
borderColor: 'transparent',
textStyle: { color: '#fff' },
axisPointer: { type: 'shadow' },
formatter: function(params) {
let result = params[0].axisValue + '<br/>';
params.forEach(param => {
const prefix = param.seriesName === '收入' ? '+' : (param.seriesName === '支出' ? '-' : '');
const color = param.seriesName === '收入' ? '#00c853' : (param.seriesName === '支出' ? '#ff3d00' : '#0052ff');
result += `<span style="color:${color}">●</span> ${param.seriesName}: ${prefix}¥${param.value.toLocaleString()}<br/>`;
});
return result;
},
transitionDuration: 0.3
},
legend: {
data: ['收入', '支出', '盈余'],
bottom: 0,
textStyle: { color: '#737373' },
animationDurationUpdate: 300
},
grid: {
left: '3%',
right: '4%',
bottom: '15%',
top: '10%',
containLabel: true
},
xAxis: {
type: 'category',
data: compareData.months,
axisLine: { lineStyle: { color: '#e5e5e5' } },
axisLabel: { color: '#737373', fontSize: 13 }
},
yAxis: {
type: 'value',
axisLine: { show: false },
splitLine: { lineStyle: { color: '#f5f5f5' } },
axisLabel: {
color: '#737373',
formatter: function(value) {
return '¥' + (value / 1000) + 'k';
}
}
},
series: [
{
name: '收入',
type: 'bar',
barWidth: '22%',
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#00c853' },
{ offset: 1, color: '#00a844' }
]),
borderRadius: [4, 4, 0, 0]
},
data: compareData.income,
animationDelay: function(idx) {
return idx * 200;
},
emphasis: {
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#00e05c' },
{ offset: 1, color: '#00c853' }
]),
shadowBlur: 15,
shadowColor: 'rgba(0, 200, 83, 0.4)'
},
scale: true,
scaleSize: 8
}
},
{
name: '支出',
type: 'bar',
barWidth: '22%',
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#ff3d00' },
{ offset: 1, color: '#e63600' }
]),
borderRadius: [4, 4, 0, 0]
},
data: compareData.expense,
animationDelay: function(idx) {
return idx * 200 + 100;
},
emphasis: {
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#ff5722' },
{ offset: 1, color: '#ff3d00' }
]),
shadowBlur: 15,
shadowColor: 'rgba(255, 61, 0, 0.4)'
},
scale: true,
scaleSize: 8
}
},
{
name: '盈余',
type: 'bar',
barWidth: '22%',
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#0052ff' },
{ offset: 1, color: '#0041cc' }
]),
borderRadius: [4, 4, 0, 0]
},
data: compareData.surplus,
animationDelay: function(idx) {
return idx * 200 + 200;
},
emphasis: {
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#336bff' },
{ offset: 1, color: '#0052ff' }
]),
shadowBlur: 15,
shadowColor: 'rgba(0, 82, 255, 0.4)'
},
scale: true,
scaleSize: 8
}
}
]
};
compareChart.setOption(option);
}
/**
* 显示Toast提示
* @param {string} message - 提示消息
*/
function showToast(message) {
const toast = document.getElementById('toast');
const toastMessage = document.getElementById('toastMessage');
toastMessage.textContent = message;
toast.classList.add('show');
setTimeout(() => {
toast.classList.remove('show');
}, 2500);
}
/**
* 图表类型与标题映射
*/
const chartTypeTitles = {
line: '月度收支趋势',
bar: '月度收支趋势',
pie: '支出构成'
};
/**
* 控制图表卡片显示/隐藏
* @param {string} chartType - 当前图表类型
*/
function updateChartCardVisibility(chartType) {
document.querySelectorAll('.chart-card').forEach(card => {
const visibleTypes = card.dataset.visible.split(',');
card.style.display = visibleTypes.includes(chartType) ? '' : 'none';
});
// 动态更新标题
const trendTitle = document.getElementById('trend-title');
if (trendTitle) {
trendTitle.textContent = chartTypeTitles[chartType];
}
// 切换后延迟执行 resize,确保容器尺寸正确
setTimeout(() => {
if (chartType === 'pie') {
expensePieChart && expensePieChart.resize();
expensePieChart && expensePieChart.setOption(expensePieChart.getOption());
} else {
trendChart && trendChart.resize();
compareChart && compareChart.resize();
}
}, 100);
}
/**
* 绑定事件监听
*/
function bindEvents() {
// 导出按钮点击
document.getElementById('exportBtn').addEventListener('click', function() {
showToast('导出成功');
});
// 图表类型切换
document.querySelectorAll('.chart-type-btn').forEach(btn => {
btn.addEventListener('click', function() {
// 更新激活状态
document.querySelectorAll('.chart-type-btn').forEach(b => {
b.classList.remove('active');
b.setAttribute('aria-selected', 'false');
});
this.classList.add('active');
this.setAttribute('aria-selected', 'true');
// 更新图表类型并重绘
const chartType = this.dataset.type;
currentTrendType = chartType;
renderTrendChart();
updateChartCardVisibility(chartType);
});
});
// 月/年切换
document.querySelectorAll('.period-tab').forEach(tab => {
tab.addEventListener('click', function() {
document.querySelectorAll('.period-tab').forEach(t => t.classList.remove('active'));
this.classList.add('active');
currentPeriod = this.dataset.period;
// 实际项目中这里会根据period重新请求数据
// 目前为原型演示,数据不变
});
});
// 窗口大小变化时,重新渲染图表(响应式resize)
window.addEventListener('resize', function() {
trendChart && trendChart.resize();
expensePieChart && expensePieChart.resize();
compareChart && compareChart.resize();
});
// 监听侧边栏折叠事件,重新渲染图表
document.addEventListener('sidebarToggle', function(e) {
// 延迟resize,等待CSS过渡完成
setTimeout(function() {
trendChart && trendChart.resize();
expensePieChart && expensePieChart.resize();
compareChart && compareChart.resize();
}, 350);
});
}
/**
* 页面初始化
*/
document.addEventListener('DOMContentLoaded', function() {
initTrendChart();
initExpensePieChart();
initCompareChart();
bindEvents();
// 设置初始卡片可见性(默认折线图)
updateChartCardVisibility('line');
});
</script>
<!-- 侧边栏折叠功能 -->
<script src="./js/sidebar.js"></script>
</body>
</html>