本文将带你搭建一个完整的 Web 报表平台:Python Flask 提供后端 API,前端使用 ECharts 渲染图表,数据存储在 SQLite 中,支持按日期筛选和图表联动。

项目结构

1
2
3
4
5
6
report_platform/
├── app.py # Flask 主应用
├── database.py # 数据库操作
├── templates/
│ └── index.html # 报表页面
└── requirements.txt # 依赖

1. 后端:Flask API

首先创建一个 Flask 应用,提供数据接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# app.py
from flask import Flask, render_template, jsonify, request
from database import get_db, query_data

app = Flask(__name__)

@app.route('/')
def index():
return render_template('index.html')

@app.route('/api/summary')
def api_summary():
"""概览指标"""
db = get_db()
cur = db.execute('''
SELECT
COUNT(*) as total_orders,
ROUND(SUM(amount), 2) as total_amount,
ROUND(AVG(amount), 2) as avg_amount,
COUNT(DISTINCT customer_id) as total_customers
FROM orders
WHERE order_date BETWEEN ? AND ?
''', (request.args.get('start', '2025-01-01'),
request.args.get('end', '2025-12-31')))
row = cur.fetchone()
return jsonify(dict(row))

@app.route('/api/monthly_trend')
def api_monthly_trend():
"""月度销售趋势"""
db = get_db()
cur = db.execute('''
SELECT strftime('%Y-%m', order_date) as month,
ROUND(SUM(amount), 2) as amount
FROM orders
GROUP BY month ORDER BY month
''')
rows = cur.fetchall()
return jsonify({
'months': [r['month'] for r in rows],
'values': [r['amount'] for r in rows]
})

@app.route('/api/category_breakdown')
def api_category_breakdown():
"""品类销售分布"""
db = get_db()
cur = db.execute('''
SELECT category, ROUND(SUM(amount), 2) as value
FROM orders GROUP BY category
''')
return jsonify([dict(r) for r in cur.fetchall()])

@app.route('/api/top_regions')
def api_top_regions():
"""区域销售排名"""
db = get_db()
cur = db.execute('''
SELECT region, ROUND(SUM(amount), 2) as value,
COUNT(*) as orders
FROM orders GROUP BY region
ORDER BY value DESC LIMIT 10
''')
return jsonify([dict(r) for r in cur.fetchall()])

if __name__ == '__main__':
app.run(debug=True, port=5000)

2. 数据库模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# database.py
import sqlite3
import random
from datetime import datetime, timedelta

DB_PATH = 'sales.db'

def get_db():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn

def init_db():
"""建表并生成模拟数据"""
conn = get_db()
conn.execute('''
CREATE TABLE IF NOT EXISTS orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_date TEXT NOT NULL,
customer_id TEXT NOT NULL,
category TEXT NOT NULL,
region TEXT NOT NULL,
amount REAL NOT NULL
)
''')

# 生成一年模拟数据(约5000条)
categories = ['电子产品', '家居用品', '服装鞋帽', '食品饮料', '其他']
regions = ['华东', '华南', '华北', '华中', '西南', '西北', '东北']
base = datetime(2025, 1, 1)

data = []
for i in range(5000):
day_offset = random.randint(0, 364)
data.append((
(base + timedelta(days=day_offset)).strftime('%Y-%m-%d'),
f'C{random.randint(1000, 9999)}',
random.choice(categories),
random.choice(regions),
round(random.uniform(50, 5000), 2)
))

conn.executemany(
'INSERT INTO orders (order_date, customer_id, category, region, amount) VALUES (?,?,?,?,?)',
data
)
conn.commit()
conn.close()

def query_data(sql, params=()):
db = get_db()
cur = db.execute(sql, params)
return cur.fetchall()

if __name__ == '__main__':
init_db()
print('数据库初始化完成')

3. 前端:报表页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
<!-- templates/index.html -->
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>销售报表平台</title>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script>
<style>
* { margin:0; padding:0; box-sizing:border-box; }
body { font-family: 'Microsoft YaHei',sans-serif; background:#f5f7fa; }
.header {
background: linear-gradient(135deg, #0a1628, #1a3a5c);
color: #fff; padding: 16px 24px;
display: flex; align-items: center; justify-content: space-between;
}
.header h1 { font-size: 20px; }
.filter-bar { padding: 12px 24px; background:#fff; display:flex; gap:12px; align-items:center; border-bottom:1px solid #e8ecf1; }
.filter-bar input { border:1px solid #d0d5dd; border-radius:4px; padding:6px 12px; }
.filter-bar button { background:#1a3a5c; color:#fff; border:none; padding:7px 20px; border-radius:4px; cursor:pointer; }
.kpi-row { display:grid; grid-template-columns:repeat(4, 1fr); gap:16px; padding:16px 24px; }
.kpi-card {
background:#fff; border-radius:8px; padding:20px;
box-shadow:0 1px 3px rgba(0,0,0,0.06);
}
.kpi-card .label { font-size:13px; color:#8899aa; margin-bottom:8px; }
.kpi-card .value { font-size:28px; font-weight:bold; color:#1a3a5c; }
.chart-row { display:grid; grid-template-columns:2fr 1fr; gap:16px; padding:0 24px 16px; }
.chart-panel { background:#fff; border-radius:8px; padding:16px; box-shadow:0 1px 3px rgba(0,0,0,0.06); }
.full-row { display:grid; grid-template-columns:1fr; padding:0 24px 16px; }
.chart-box { width:100%; height:350px; }
</style>
</head>
<body>
<div class="header">
<h1>销售数据报表平台</h1>
<span>数据更新时间:<span id="updateTime">--</span></span>
</div>

<div class="filter-bar">
<label>起始日期</label>
<input type="date" id="startDate" value="2025-01-01">
<label>结束日期</label>
<input type="date" id="endDate" value="2025-12-31">
<button onclick="refreshAll()">查询</button>
</div>

<div class="kpi-row">
<div class="kpi-card">
<div class="label">订单总量</div>
<div class="value" id="kpiOrders">--</div>
</div>
<div class="kpi-card">
<div class="label">销售总额</div>
<div class="value" id="kpiAmount">--</div>
</div>
<div class="kpi-card">
<div class="label">客单价</div>
<div class="value" id="kpiAvg">--</div>
</div>
<div class="kpi-card">
<div class="label">客户数</div>
<div class="value" id="kpiCustomers">--</div>
</div>
</div>

<div class="chart-row">
<div class="chart-panel">
<h3 style="margin-bottom:12px;">月度销售趋势</h3>
<div class="chart-box" id="trendChart"></div>
</div>
<div class="chart-panel">
<h3 style="margin-bottom:12px;">品类占比</h3>
<div class="chart-box" id="pieChart"></div>
</div>
</div>

<div class="chart-row">
<div class="chart-panel">
<h3 style="margin-bottom:12px;">区域销售排名</h3>
<div class="chart-box" id="barChart"></div>
</div>
<div class="chart-panel" style="overflow-y:auto;max-height:380px;" id="regionTable">
<table style="width:100%;border-collapse:collapse;" id="regionTbody"></table>
</div>
</div>

<script>
const trendChart = echarts.init(document.getElementById('trendChart'));
const pieChart = echarts.init(document.getElementById('pieChart'));
const barChart = echarts.init(document.getElementById('barChart'));

async function fetchJSON(url) {
const res = await fetch(url);
return res.json();
}

async function loadSummary() {
const params = new URLSearchParams({
start: document.getElementById('startDate').value,
end: document.getElementById('endDate').value
});
const data = await fetchJSON(`/api/summary?${params}`);
document.getElementById('kpiOrders').textContent = data.total_orders.toLocaleString();
document.getElementById('kpiAmount').textContent = '¥' + (data.total_amount/10000).toFixed(1) + '万';
document.getElementById('kpiAvg').textContent = '¥' + Number(data.avg_amount).toFixed(0);
document.getElementById('kpiCustomers').textContent = data.total_customers.toLocaleString();
}

async function loadTrend() {
const data = await fetchJSON('/api/monthly_trend');
trendChart.setOption({
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: data.months },
yAxis: { type: 'value', name: '万元',
axisLabel: { formatter: v => (v/10000).toFixed(0) } },
series: [{
type: 'line', smooth: true, data: data.values,
areaStyle: { color: 'rgba(26,58,92,0.08)' },
lineStyle: { color: '#1a3a5c', width: 2 },
itemStyle: { color: '#1a3a5c' }
}]
});
}

async function loadPie() {
const data = await fetchJSON('/api/category_breakdown');
pieChart.setOption({
tooltip: { trigger: 'item' },
series: [{
type: 'pie', radius: ['45%', '72%'],
data: data,
label: { formatter: '{b}\n{d}%' }
}]
});
}

async function loadRegion() {
const data = await fetchJSON('/api/top_regions');
barChart.setOption({
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: data.map(d => d.region) },
yAxis: { type: 'value', name: '万元',
axisLabel: { formatter: v => (v/10000).toFixed(0) } },
series: [{
type: 'bar', data: data.map(d => d.value),
itemStyle: { borderRadius: [4,4,0,0],
color: new echarts.graphic.LinearGradient(0,0,0,1,[
{ offset:0, color:'#4a90d9' }, { offset:1, color:'#1a3a5c' }
]) }
}]
});

// 区域数据表格
const rows = data.map((d, i) => `
<tr style="border-bottom:1px solid #f0f0f0;">
<td style="padding:8px;">
<span style="display:inline-block;width:22px;height:22px;line-height:22px;
text-align:center;border-radius:50%;font-size:12px;
background:${i<3?'#1a3a5c':'#e8ecf1'};color:${i<3?'#fff':'#666'};margin-right:8px;">${i+1}</span>
${d.region}
</td>
<td style="text-align:right;padding:8px;font-weight:bold;">¥${(d.value/10000).toFixed(1)}万</td>
<td style="text-align:right;padding:8px;color:#999;">${d.orders}单</td>
</tr>
`).join('');
document.getElementById('regionTbody').innerHTML = `
<tr style="background:#f5f7fa;font-weight:bold;">
<th style="padding:10px 8px;text-align:left;">区域</th>
<th style="padding:10px 8px;text-align:right;">销售额</th>
<th style="padding:10px 8px;text-align:right;">订单数</th>
</tr>
${rows}
`;
}

async function refreshAll() {
document.getElementById('updateTime').textContent = new Date().toLocaleString();
await Promise.all([loadSummary(), loadTrend(), loadPie(), loadRegion()]);
}

window.addEventListener('resize', () => {
[trendChart, pieChart, barChart].forEach(c => c.resize());
});

refreshAll();
</script>
</body>
</html>

4. 启动运行

1
2
3
pip install flask
python database.py # 初始化数据库,生成模拟数据
python app.py # 启动 Flask 服务

打开 http://localhost:5000,即可看到完整的报表平台。

架构要点

组件 技术选型 作用
后端 Flask RESTful API,查询 SQLite 返回 JSON
数据库 SQLite 轻量嵌入式,适合个人/小团队
前端 ECharts 5 折线图+饼图+柱状图+排名表
通信 fetch + JSON 前端定时请求后端 API 刷新数据
部署 Gunicorn + Nginx 生产环境上线方案

扩展到生产环境

1
2
3
4
5
6
7
8
9
# 使用 Gunicorn 启动
# gunicorn -w 4 -b 0.0.0.0:5000 app:app

# 生产依赖
# flask
# gunicorn
# pandas # 如果需要复杂数据处理
# openpyxl # Excel 导入导出
# apscheduler # 定时任务刷新缓存

这套架构可直接作为企业报表平台的基础框架,后续可按需扩展用户认证、权限管理、报表导出(PDF/Excel)、定时邮件推送等功能。