ECharts数据大屏 - 完整知识点
目录
- 数据大屏简介
- 数据大屏设计方法
- 项目实战:静态大屏制作
- 项目实战:动态大屏制作
- 本章小结
一、数据大屏简介
1.1 什么是数据大屏
数据大屏是将业务数据通过可视化图表、图形、指标等形式,集中展示在大屏幕上的解决方案。
1.2 数据大屏的特点
| 特点 | 说明 |
|---|---|
| 可视化 | 图形化展示数据,直观易懂 |
| 实时性 | 动态刷新,展示最新数据 |
| 全景性 | 多维度、多指标同时展示 |
| 交互性 | 支持鼠标悬停、点击等交互 |
1.3 适用场景
- 企业经营管理驾驶舱
- 智慧城市指挥中心
- 电商实时交易监控
- 工厂生产监控大屏
- 疫情防控数据平台
二、数据大屏设计方法
2.1 设计原则
原则一:清晰优先
html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>清晰优先示例</title>
<style>
/* 确保字体清晰可读 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #0a0f1a; /* 深色背景,减少视觉疲劳 */
font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif;
}
/* 大屏容器 - 清晰的信息层级 */
.dashboard {
display: grid;
grid-template-columns: repeat(4, 1fr); /* 四列网格布局 */
gap: 20px; /* 图表间距20像素 */
padding: 20px;
max-width: 1920px; /* 适配大屏宽度 */
margin: 0 auto;
}
/* 每个图表卡片 - 清晰边框和背景 */
.chart-card {
background: rgba(20, 28, 40, 0.8); /* 半透明深色背景 */
border-radius: 12px; /* 圆角边框 */
padding: 15px;
border: 1px solid rgba(64, 158, 255, 0.3); /* 蓝色边框,增加清晰度 */
backdrop-filter: blur(10px); /* 背景模糊效果 */
}
/* 图表标题 - 清晰醒目 */
.chart-title {
color: #ffffff;
font-size: 18px;
font-weight: bold;
margin-bottom: 15px;
padding-left: 12px;
border-left: 4px solid #409eff; /* 左侧蓝色装饰条 */
}
</style>
</head>
<body>
<div class="dashboard">
<div class="chart-card">
<div class="chart-title">销售额趋势</div>
<div id="chart1" style="height: 300px;"></div>
</div>
</div>
</body>
</html>
原则二:一致性
html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>一致性示例</title>
<style>
/* 统一的颜色系统 */
:root {
/* 主色调 - 蓝色系 */
--primary-color: #409eff;
--primary-light: #66b1ff;
--primary-dark: #1890ff;
/* 辅助色 - 绿色、橙色、红色 */
--success-color: #67c23a;
--warning-color: #e6a23c;
--danger-color: #f56c6c;
/* 中性色 */
--text-primary: #ffffff;
--text-secondary: rgba(255, 255, 255, 0.7);
--border-color: rgba(255, 255, 255, 0.1);
}
/* 统一的卡片样式 */
.card {
background: linear-gradient(135deg, rgba(32, 42, 58, 0.9), rgba(20, 28, 40, 0.9));
border-radius: 12px;
border: 1px solid var(--border-color);
transition: all 0.3s ease; /* 统一过渡效果 */
}
/* 统一的标题样式 */
.card-title {
font-size: 16px;
color: var(--text-primary);
margin-bottom: 16px;
position: relative;
}
/* 统一的数值样式 */
.card-value {
font-size: 32px;
font-weight: bold;
color: var(--primary-color);
font-family: 'DIN', 'Arial', sans-serif;
}
/* 统一的悬停效果 */
.card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
border-color: var(--primary-color);
}
</style>
</head>
<body>
<!-- 一致性体现在所有卡片使用相同的样式类 -->
<div class="card">
<div class="card-title">总销售额</div>
<div class="card-value">¥1,234,567</div>
</div>
</body>
</html>
原则三:层次感
html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>层次感示例</title>
<style>
/* 通过视觉重量建立层次 */
.dashboard-layer {
display: flex;
flex-direction: column;
height: 1080px;
background: #0a0e27;
}
/* 顶层 - 最重要的KPI指标 */
.kpi-layer {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
padding: 20px;
height: 180px; /* 较高,突出重要性 */
}
.kpi-card {
background: linear-gradient(135deg, #1a2a3a, #0f1a24);
border-radius: 16px;
padding: 20px;
border-top: 4px solid #409eff; /* 顶部强调色条 */
box-shadow: 0 8px 20px rgba(0,0,0,0.3);
}
.kpi-value {
font-size: 48px; /* 最大字体,最醒目 */
font-weight: bold;
color: #409eff;
text-shadow: 0 0 10px rgba(64,158,255,0.5);
}
/* 中间层 - 主要图表 */
.main-charts-layer {
display: grid;
grid-template-columns: 2fr 1fr; /* 主图表占2/3,副图表占1/3 */
gap: 20px;
padding: 0 20px;
height: 500px;
}
.main-chart {
background: rgba(30, 40, 55, 0.6);
border-radius: 12px;
padding: 15px;
}
/* 底层 - 辅助信息 */
.sub-info-layer {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
padding: 20px;
height: 300px;
}
.sub-card {
background: rgba(30, 40, 55, 0.4); /* 透明度更低,视觉重量轻 */
border-radius: 8px;
padding: 12px;
}
.sub-value {
font-size: 20px; /* 较小字体 */
color: rgba(255,255,255,0.7);
}
</style>
</head>
<body>
<div class="dashboard-layer">
<div class="kpi-layer">
<div class="kpi-card">
<div class="kpi-value">1,234</div>
<div>总订单数</div>
</div>
</div>
<div class="main-charts-layer">
<div class="main-chart">主图表区域</div>
</div>
<div class="sub-info-layer">
<div class="sub-card">辅助信息区域</div>
</div>
</div>
</body>
</html>
2.2 设计流程
需求分析
数据梳理
原型设计
图表选型
界面开发
数据对接
测试优化
部署上线
三、项目实战:静态大屏制作
3.1 准备工作
3.1.1 项目结构
echarts-dashboard/
├── index.html # 主页面
├── css/
│ └── style.css # 样式文件
├── js/
│ ├── echarts.min.js # ECharts库
│ ├── data.js # 静态数据
│ └── charts.js # 图表配置
└── assets/
└── images/ # 图片资源
3.1.2 引入ECharts
html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>电商数据大屏</title>
<!-- 引入ECharts核心库 -->
<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script>
<!-- 引入数据文件 -->
<script src="js/data.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
overflow: hidden; /* 隐藏滚动条,大屏全屏显示 */
font-family: 'Microsoft YaHei', 'PingFang SC', 'Helvetica Neue', Arial, sans-serif;
}
/* 大屏主容器 - 1920x1080标准分辨率 */
.screen {
width: 1920px;
height: 1080px;
background: url('assets/images/bg.jpg') no-repeat center center;
background-size: cover; /* 背景图片铺满 */
position: relative;
}
/* 头部区域 */
.header {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 80px;
background: linear-gradient(180deg, rgba(0,0,0,0.6), transparent);
display: flex;
justify-content: center;
align-items: center;
}
.title {
font-size: 36px;
color: #fff;
text-shadow: 0 0 10px rgba(64,158,255,0.8);
letter-spacing: 5px; /* 字间距 */
}
/* 图表网格布局 */
.charts-grid {
position: absolute;
top: 100px;
left: 20px;
right: 20px;
bottom: 20px;
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(3, 1fr);
gap: 15px;
}
/* 图表卡片通用样式 */
.chart-box {
background: rgba(10, 20, 40, 0.6);
backdrop-filter: blur(8px);
border-radius: 12px;
border: 1px solid rgba(64,158,255,0.3);
padding: 12px;
}
/* 各图表位置分配 */
.chart-1 { grid-column: 1 / 3; } /* 销售额趋势 - 占两列 */
.chart-2 { grid-column: 3 / 4; } /* 品类占比 - 占一列 */
.chart-3 { grid-column: 4 / 5; } /* 关键指标 - 占一列 */
.chart-4 { grid-column: 1 / 2; } /* 区域销售 - 占一列 */
.chart-5 { grid-column: 2 / 5; } /* 排行榜 - 占三列 */
.chart-container {
width: 100%;
height: calc(100% - 30px);
}
.chart-title {
color: #fff;
font-size: 16px;
margin-bottom: 10px;
padding-left: 10px;
border-left: 3px solid #409eff;
}
</style>
</head>
<body>
<div class="screen">
<div class="header">
<div class="title">📊 智慧电商数据大屏</div>
</div>
<div class="charts-grid">
<!-- 图表1: 销售额趋势 -->
<div class="chart-box chart-1">
<div class="chart-title">销售额趋势(万元)</div>
<div id="trendChart" class="chart-container"></div>
</div>
<!-- 图表2: 品类占比 -->
<div class="chart-box chart-2">
<div class="chart-title">销售额品类占比</div>
<div id="categoryChart" class="chart-container"></div>
</div>
<!-- 图表3: 关键指标 -->
<div class="chart-box chart-3">
<div class="chart-title">关键运营指标</div>
<div id="indicatorChart" class="chart-container"></div>
</div>
<!-- 图表4: 区域销售 -->
<div class="chart-box chart-4">
<div class="chart-title">区域销售额分布</div>
<div id="regionChart" class="chart-container"></div>
</div>
<!-- 图表5: 热销排行榜 -->
<div class="chart-box chart-5">
<div class="chart-title">热销商品TOP10</div>
<div id="rankChart" class="chart-container"></div>
</div>
</div>
</div>
<script src="js/charts.js"></script>
</body>
</html>
3.2 图表制作
3.2.1 数据文件 (js/data.js)
javascript
/**
* 电商大屏静态数据
* 包含所有图表所需的模拟数据
*/
// 销售额趋势数据 - 近12个月
const salesTrendData = {
months: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
sales: [320, 280, 350, 410, 480, 620, 710, 850, 920, 1080, 1350, 1680], // 销售额(万元)
orders: [3200, 2980, 3650, 4320, 5120, 6580, 7420, 8920, 9650, 11200, 13800, 16500] // 订单量
};
// 品类销售数据
const categoryData = {
names: ['手机数码', '服装服饰', '美妆个护', '家用电器', '食品生鲜', '家居用品'],
values: [850, 620, 430, 380, 290, 210], // 销售额(万元)
colors: ['#409eff', '#67c23a', '#e6a23c', '#f56c6c', '#909399', '#9b59b6']
};
// 区域销售数据
const regionData = {
regions: ['华东', '华南', '华北', '西南', '华中', '东北', '西北'],
sales: [1250, 980, 760, 540, 620, 380, 290]
};
// 热销商品排行
const productRankData = {
products: [
'iPhone 15 Pro Max', '华为 Mate 60 Pro', '小米 14 Ultra',
'耐克 Air Max', '雅诗兰黛小棕瓶', '海尔冰箱 BCD-500',
'Adidas运动鞋', '三只松鼠坚果礼盒', 'SK-II神仙水', '小米扫地机器人'
],
sales: [25800, 22300, 19800, 17500, 15600, 14200, 12800, 11500, 9800, 8600] // 销量件数
};
// 关键指标数据
const kpiData = {
totalSales: 1680, // 本月总销售额(万元)
totalOrders: 16500, // 本月总订单数
avgPrice: 1018, // 平均客单价(元)
growth: 24.5, // 同比增长率(%)
conversion: 3.8, // 转化率(%)
refund: 2.1 // 退款率(%)
};
3.2.2 图表配置文件 (js/charts.js)
javascript
/**
* ECharts图表配置和渲染
* 包含所有图表的完整配置
*/
// ==================== 等待DOM加载完成 ====================
document.addEventListener('DOMContentLoaded', function() {
// 1. 销售额趋势图 - 折线图 + 柱状图组合
function renderTrendChart() {
// 获取图表容器DOM元素
const chartDom = document.getElementById('trendChart');
// 初始化ECharts实例
const myChart = echarts.init(chartDom);
// 配置图表选项
const option = {
// 背景色透明(继承父容器)
backgroundColor: 'transparent',
// 提示框组件(鼠标悬停显示数据)
tooltip: {
trigger: 'axis', // 坐标轴触发
axisPointer: { type: 'shadow' }, // 阴影指示器
backgroundColor: 'rgba(0,0,0,0.8)',
borderColor: '#409eff',
textStyle: { color: '#fff' }
},
// 图例组件(标识不同系列)
legend: {
data: ['销售额(万元)', '订单量(单)'],
textStyle: { color: '#fff' },
right: 20,
top: 0
},
// 网格配置(图表绘图区域)
grid: {
left: '8%',
right: '8%',
top: '15%',
bottom: '8%',
containLabel: true
},
// X轴配置
xAxis: {
type: 'category', // 类目轴
data: salesTrendData.months, // 月份数据
axisLabel: {
color: '#fff',
rotate: 0 // 标签旋转角度
},
axisLine: {
lineStyle: { color: 'rgba(255,255,255,0.3)' }
},
axisTick: { show: false } // 隐藏刻度线
},
// Y轴配置(左侧 - 销售额)
yAxis: [
{
type: 'value',
name: '销售额(万元)',
nameTextStyle: { color: '#fff' },
axisLabel: { color: '#fff' },
splitLine: {
lineStyle: { color: 'rgba(255,255,255,0.1)' }
}
},
{
type: 'value',
name: '订单量(单)',
nameTextStyle: { color: '#fff' },
axisLabel: { color: '#fff' },
splitLine: { show: false } // 右侧Y轴不显示分割线
}
],
// 系列数据(图表类型和数据)
series: [
{
name: '销售额(万元)',
type: 'line', // 折线图
data: salesTrendData.sales,
smooth: true, // 平滑曲线
symbol: 'circle', // 标记点形状
symbolSize: 8, // 标记点大小
lineStyle: {
width: 3,
color: '#409eff',
shadowBlur: 10,
shadowColor: '#409eff'
},
areaStyle: { // 面积填充
opacity: 0.3,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#409eff' },
{ offset: 1, color: 'rgba(64,158,255,0)' }
])
},
itemStyle: { color: '#409eff' }
},
{
name: '订单量(单)',
type: 'bar', // 柱状图
yAxisIndex: 1, // 使用右侧Y轴
data: salesTrendData.orders,
barWidth: '40%', // 柱子宽度
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#67c23a' },
{ offset: 1, color: '#529b2e' }
]),
borderRadius: [4, 4, 0, 0] // 上边框圆角
}
}
]
};
// 应用配置并渲染图表
myChart.setOption(option);
// 添加窗口自适应(图表跟随容器大小变化)
window.addEventListener('resize', () => myChart.resize());
}
// 2. 品类占比图 - 饼图(带环形效果)
function renderCategoryChart() {
const chartDom = document.getElementById('categoryChart');
const myChart = echarts.init(chartDom);
// 准备饼图数据格式
const pieData = categoryData.names.map((name, index) => ({
name: name,
value: categoryData.values[index],
itemStyle: { color: categoryData.colors[index] }
}));
const option = {
backgroundColor: 'transparent',
tooltip: {
trigger: 'item',
formatter: '{b}: {d}% ({c}万元)', // 自定义提示格式
backgroundColor: 'rgba(0,0,0,0.8)'
},
legend: {
orient: 'vertical', // 垂直排列
right: 10,
top: 'center',
textStyle: { color: '#fff' },
itemWidth: 12,
itemHeight: 12,
formatter: (name) => {
// 在图例中显示百分比
const total = categoryData.values.reduce((a, b) => a + b, 0);
const item = categoryData.names.findIndex(n => n === name);
const percent = ((categoryData.values[item] / total) * 100).toFixed(1);
return `${name} ${percent}%`;
}
},
series: [{
name: '品类销售',
type: 'pie', // 饼图
radius: ['45%', '70%'], // 内半径和外半径(环形效果)
center: ['40%', '50%'], // 圆心位置
avoidLabelOverlap: false,
label: {
show: true,
position: 'outside',
formatter: '{b}: {d}%',
color: '#fff'
},
emphasis: { // 高亮效果
scale: true,
scaleSize: 10
},
data: pieData,
itemStyle: {
borderRadius: 8, // 圆角
borderColor: '#0a1428',
borderWidth: 2
}
}]
};
myChart.setOption(option);
window.addEventListener('resize', () => myChart.resize());
}
// 3. 关键指标图 - 仪表盘/进度条效果
function renderIndicatorChart() {
const chartDom = document.getElementById('indicatorChart');
const myChart = echarts.init(chartDom);
// 使用gauge仪表盘显示关键指标
const option = {
backgroundColor: 'transparent',
series: [
{
name: '同比增长率',
type: 'gauge', // 仪表盘
center: ['25%', '50%'],
radius: '70%',
min: 0,
max: 50,
splitNumber: 5,
progress: {
show: true,
width: 12,
itemStyle: { color: '#409eff' }
},
axisLine: {
lineStyle: { width: 12, color: [[0.5, '#67c23a']] }
},
axisTick: { show: false },
splitLine: { show: false },
axisLabel: { show: false },
pointer: { show: false },
detail: {
offsetCenter: [0, 20],
valueAnimation: true,
fontSize: 16,
color: '#fff'
},
title: { show: true, offsetCenter: [0, -20], color: '#fff' },
data: [{ value: kpiData.growth, name: '同比增长率%' }]
},
{
name: '转化率',
type: 'gauge',
center: ['50%', '50%'],
radius: '70%',
min: 0,
max: 10,
progress: {
show: true,
width: 12,
itemStyle: { color: '#67c23a' }
},
axisLine: { lineStyle: { width: 12 } },
axisTick: { show: false },
splitLine: { show: false },
axisLabel: { show: false },
pointer: { show: false },
detail: {
offsetCenter: [0, 20],
valueAnimation: true,
fontSize: 16,
color: '#fff'
},
title: { show: true, offsetCenter: [0, -20], color: '#fff' },
data: [{ value: kpiData.conversion, name: '转化率%' }]
},
{
name: '退款率',
type: 'gauge',
center: ['75%', '50%'],
radius: '70%',
min: 0,
max: 10,
progress: {
show: true,
width: 12,
itemStyle: { color: '#f56c6c' }
},
axisLine: { lineStyle: { width: 12 } },
axisTick: { show: false },
splitLine: { show: false },
axisLabel: { show: false },
pointer: { show: false },
detail: {
offsetCenter: [0, 20],
valueAnimation: true,
fontSize: 16,
color: '#fff'
},
title: { show: true, offsetCenter: [0, -20], color: '#fff' },
data: [{ value: kpiData.refund, name: '退款率%' }]
}
]
};
myChart.setOption(option);
window.addEventListener('resize', () => myChart.resize());
}
// 4. 区域销售额图 - 横向柱状图
function renderRegionChart() {
const chartDom = document.getElementById('regionChart');
const myChart = echarts.init(chartDom);
const option = {
backgroundColor: 'transparent',
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
formatter: '{b}: {c}万元'
},
grid: {
left: '15%',
right: '5%',
containLabel: true
},
xAxis: {
type: 'value',
name: '销售额(万元)',
nameTextStyle: { color: '#fff' },
axisLabel: { color: '#fff' },
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } }
},
yAxis: {
type: 'category',
data: regionData.regions,
axisLabel: { color: '#fff', fontSize: 12 },
axisLine: { show: false },
axisTick: { show: false }
},
series: [{
name: '销售额',
type: 'bar',
data: regionData.sales,
barWidth: '60%',
label: {
show: true,
position: 'right',
formatter: '{c}万',
color: '#fff'
},
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: '#409eff' },
{ offset: 1, color: '#9b59b6' }
]),
borderRadius: [0, 4, 4, 0]
}
}]
};
myChart.setOption(option);
window.addEventListener('resize', () => myChart.resize());
}
// 5. 热销排行榜 - 条形图
function renderRankChart() {
const chartDom = document.getElementById('rankChart');
const myChart = echarts.init(chartDom);
// 反转数据以便从上到下显示(第一名在最上面)
const reversedProducts = [...productRankData.products].reverse();
const reversedSales = [...productRankData.sales].reverse();
const option = {
backgroundColor: 'transparent',
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
formatter: '{b}: {c}件'
},
grid: {
left: '20%',
right: '8%',
top: '5%',
bottom: '5%',
containLabel: true
},
xAxis: {
type: 'value',
name: '销量(件)',
nameTextStyle: { color: '#fff' },
axisLabel: { color: '#fff' },
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } }
},
yAxis: {
type: 'category',
data: reversedProducts,
axisLabel: {
color: '#fff',
fontSize: 11,
// 商品名称过长时截断
formatter: (value) => value.length > 12 ? value.slice(0, 10) + '...' : value
},
axisTick: { show: false },
axisLine: { show: false }
},
series: [{
name: '销量',
type: 'bar',
data: reversedSales,
barWidth: '50%',
label: {
show: true,
position: 'right',
formatter: '{c}件',
color: '#fff',
fontSize: 11
},
itemStyle: {
// 根据排名设置不同颜色(前3名金色、银色、铜色)
color: (params) => {
const rank = 10 - params.dataIndex;
if (rank === 1) return '#ffd700';
if (rank === 2) return '#c0c0c0';
if (rank === 3) return '#cd7f32';
return '#409eff';
},
borderRadius: [0, 4, 4, 0]
},
// 添加动画效果
animation: true,
animationDuration: 1000,
animationEasing: 'cubicOut'
}]
};
myChart.setOption(option);
window.addEventListener('resize', () => myChart.resize());
}
// 调用所有图表渲染函数
renderTrendChart();
renderCategoryChart();
renderIndicatorChart();
renderRegionChart();
renderRankChart();
console.log('所有图表已加载完成');
});
3.3 大屏展示
添加数字滚动动画效果
javascript
/**
* 数字滚动动画组件
* 为KPI指标添加从0到目标值的滚动效果
*/
class NumberRoller {
constructor(element, targetValue, duration = 2000) {
this.element = element; // DOM元素
this.targetValue = targetValue; // 目标数值
this.duration = duration; // 动画持续时间(毫秒)
this.startValue = 0; // 起始数值
this.startTime = null; // 开始时间戳
}
// 缓动函数 - easeOutCubic
easeOutCubic(t) {
return 1 - Math.pow(1 - t, 3);
}
// 开始动画
start() {
this.startTime = performance.now();
this.update();
}
// 更新数值
update() {
const now = performance.now();
const elapsed = now - this.startTime;
let progress = Math.min(elapsed / this.duration, 1);
progress = this.easeOutCubic(progress);
// 计算当前值
const currentValue = this.startValue + (this.targetValue - this.startValue) * progress;
// 格式化显示(万元保留1位小数,订单数取整)
if (this.element.id === 'totalSales') {
this.element.textContent = currentValue.toFixed(1);
} else if (this.element.id === 'totalOrders') {
this.element.textContent = Math.floor(currentValue).toLocaleString();
} else {
this.element.textContent = Math.floor(currentValue);
}
// 继续动画
if (progress < 1) {
requestAnimationFrame(() => this.update());
} else {
// 动画完成,确保显示最终值
if (this.element.id === 'totalSales') {
this.element.textContent = this.targetValue.toFixed(1);
} else if (this.element.id === 'totalOrders') {
this.element.textContent = this.targetValue.toLocaleString();
} else {
this.element.textContent = this.targetValue;
}
}
}
}
// 在页面中添加KPI卡片并启动动画
function initKPI() {
// 创建KPI卡片容器
const kpiContainer = document.createElement('div');
kpiContainer.className = 'kpi-container';
kpiContainer.style.cssText = `
position: absolute;
top: 100px;
right: 20px;
width: 280px;
background: rgba(10,20,40,0.8);
backdrop-filter: blur(10px);
border-radius: 12px;
padding: 15px;
border: 1px solid rgba(64,158,255,0.3);
z-index: 10;
`;
kpiContainer.innerHTML = `
<div style="color:#fff; font-size:16px; margin-bottom:15px;">📈 核心KPI指标</div>
<div style="margin-bottom:12px;">
<div style="color:rgba(255,255,255,0.7); font-size:12px;">本月销售额</div>
<div style="font-size:32px; font-weight:bold; color:#409eff;">
<span id="totalSales">0</span> 万元
</div>
</div>
<div style="margin-bottom:12px;">
<div style="color:rgba(255,255,255,0.7); font-size:12px;">本月订单量</div>
<div style="font-size:32px; font-weight:bold; color:#67c23a;">
<span id="totalOrders">0</span> 单
</div>
</div>
<div style="margin-bottom:12px;">
<div style="color:rgba(255,255,255,0.7); font-size:12px;">平均客单价</div>
<div style="font-size:32px; font-weight:bold; color:#e6a23c;">
¥<span id="avgPrice">0</span>
</div>
</div>
<div>
<div style="color:rgba(255,255,255,0.7); font-size:12px;">同比增长</div>
<div style="font-size:32px; font-weight:bold; color:#f56c6c;">
<span id="growth">0</span>%
</div>
</div>
`;
document.querySelector('.screen').appendChild(kpiContainer);
// 启动数字滚动动画
new NumberRoller(document.getElementById('totalSales'), kpiData.totalSales, 2000).start();
new NumberRoller(document.getElementById('totalOrders'), kpiData.totalOrders, 2000).start();
new NumberRoller(document.getElementById('avgPrice'), kpiData.avgPrice, 2000).start();
new NumberRoller(document.getElementById('growth'), kpiData.growth, 2000).start();
}
// 调用KPI初始化(需要在charts.js中调用)
// initKPI();
四、项目实战:动态大屏制作
4.1 准备工作
4.1.1 引入WebSocket和Mock数据
javascript
/**
* 动态数据大屏 - WebSocket连接和数据模拟
*/
// WebSocket连接配置
const WS_CONFIG = {
url: 'ws://localhost:8080/ws/dashboard', // WebSocket服务器地址
reconnectInterval: 3000, // 重连间隔(毫秒)
maxReconnectAttempts: 10 // 最大重连次数
};
// 模拟数据生成器(用于演示,实际可从API获取)
class MockDataGenerator {
constructor() {
this.interval = null;
}
// 生成随机销售额(在基础值上下波动)
generateRandomSales(baseValue = 1000) {
const change = (Math.random() - 0.5) * 0.1; // ±5%波动
return Math.round(baseValue * (1 + change));
}
// 生成品类数据(保持总和不变)
generateCategoryData() {
const total = 2800; // 总销售额2800万元
const baseValues = [850, 620, 430, 380, 290, 230];
// 随机分配,总和保持不变
let remaining = total;
const newValues = [];
for (let i = 0; i < baseValues.length - 1; i++) {
const variance = (Math.random() - 0.5) * 100; // ±50波动
let value = baseValues[i] + variance;
value = Math.max(100, Math.min(remaining - 100, value));
newValues.push(Math.round(value));
remaining -= newValues[i];
}
newValues.push(Math.round(remaining));
return newValues;
}
// 生成区域数据
generateRegionData() {
const baseRegions = [1250, 980, 760, 540, 620, 380, 290];
return baseRegions.map(v => {
const change = (Math.random() - 0.5) * 0.08;
return Math.max(100, Math.round(v * (1 + change)));
});
}
// 生成订单趋势数据(最后一个月更新)
generateOrderTrend(lastValue = 16500) {
const change = Math.round((Math.random() - 0.5) * 500);
return Math.max(10000, lastValue + change);
}
// 生成完整的仪表盘数据
generateFullData() {
return {
timestamp: new Date().toISOString(),
salesTrend: {
months: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
sales: [320, 280, 350, 410, 480, 620, 710, 850, 920, 1080, 1350, this.generateRandomSales(1680)],
orders: [3200, 2980, 3650, 4320, 5120, 6580, 7420, 8920, 9650, 11200, 13800, this.generateOrderTrend()]
},
category: {
names: ['手机数码', '服装服饰', '美妆个护', '家用电器', '食品生鲜', '家居用品'],
values: this.generateCategoryData()
},
region: {
regions: ['华东', '华南', '华北', '西南', '华中', '东北', '西北'],
sales: this.generateRegionData()
},
kpi: {
totalSales: this.generateRandomSales(1680),
totalOrders: this.generateOrderTrend(),
conversion: (3.5 + Math.random() * 1).toFixed(1),
onlineUsers: Math.floor(500 + Math.random() * 300)
}
};
}
}
4.2 图表制作(动态版)
javascript
/**
* 动态图表管理器
* 支持WebSocket实时数据更新
*/
class DynamicChartManager {
constructor() {
this.charts = {}; // 存储所有图表实例
this.ws = null; // WebSocket连接
this.reconnectAttempts = 0; // 重连次数
this.isConnected = false; // 连接状态
}
// 初始化所有图表
initCharts() {
// 销售额趋势图(动态)
this.charts.trend = echarts.init(document.getElementById('trendChart'));
this.charts.trend.setOption(this.getTrendOption());
// 品类占比图(动态)
this.charts.category = echarts.init(document.getElementById('categoryChart'));
this.charts.category.setOption(this.getCategoryOption());
// 区域销售图(动态)
this.charts.region = echarts.init(document.getElementById('regionChart'));
this.charts.region.setOption(this.getRegionOption());
// 实时监控图(新增)
this.charts.realtime = echarts.init(document.getElementById('realtimeChart'));
this.charts.realtime.setOption(this.getRealtimeOption());
// 添加窗口自适应
window.addEventListener('resize', () => {
Object.values(this.charts).forEach(chart => chart.resize());
});
}
// 销售额趋势图配置
getTrendOption() {
return {
backgroundColor: 'transparent',
tooltip: { trigger: 'axis' },
legend: {
data: ['销售额(万元)', '订单量(单)'],
textStyle: { color: '#fff' }
},
xAxis: {
type: 'category',
data: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
axisLabel: { color: '#fff' },
axisLine: { lineStyle: { color: 'rgba(255,255,255,0.3)' } }
},
yAxis: [
{
type: 'value',
name: '销售额(万元)',
nameTextStyle: { color: '#fff' },
axisLabel: { color: '#fff' }
},
{
type: 'value',
name: '订单量(单)',
nameTextStyle: { color: '#fff' },
axisLabel: { color: '#fff' }
}
],
series: [
{
name: '销售额(万元)',
type: 'line',
smooth: true,
lineStyle: { width: 3, color: '#409eff' },
areaStyle: { opacity: 0.3, color: '#409eff' },
data: []
},
{
name: '订单量(单)',
type: 'bar',
yAxisIndex: 1,
itemStyle: { color: '#67c23a', borderRadius: [4,4,0,0] },
data: []
}
]
};
}
// 品类占比图配置
getCategoryOption() {
return {
backgroundColor: 'transparent',
tooltip: { trigger: 'item', formatter: '{b}: {d}%' },
legend: {
orient: 'vertical',
right: 10,
top: 'center',
textStyle: { color: '#fff' }
},
series: [{
type: 'pie',
radius: ['45%', '70%'],
center: ['40%', '50%'],
label: { show: true, formatter: '{b}: {d}%', color: '#fff' },
data: []
}]
};
}
// 区域销售图配置
getRegionOption() {
return {
backgroundColor: 'transparent',
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
grid: { left: '15%' },
xAxis: {
type: 'value',
name: '销售额(万元)',
nameTextStyle: { color: '#fff' },
axisLabel: { color: '#fff' }
},
yAxis: {
type: 'category',
data: ['华东', '华南', '华北', '西南', '华中', '东北', '西北'],
axisLabel: { color: '#fff' }
},
series: [{
type: 'bar',
label: { show: true, position: 'right', formatter: '{c}万' },
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: '#409eff' },
{ offset: 1, color: '#9b59b6' }
])
},
data: []
}]
};
}
// 实时监控图配置(折线图)
getRealtimeOption() {
return {
backgroundColor: 'transparent',
title: {
text: '实时在线用户数',
textStyle: { color: '#fff', fontSize: 14 }
},
tooltip: { trigger: 'axis' },
xAxis: {
type: 'category',
data: [],
axisLabel: { color: '#fff' }
},
yAxis: {
type: 'value',
name: '在线人数',
nameTextStyle: { color: '#fff' },
axisLabel: { color: '#fff' }
},
series: [{
type: 'line',
smooth: true,
lineStyle: { width: 2, color: '#e6a23c' },
areaStyle: { opacity: 0.3, color: '#e6a23c' },
data: []
}]
};
}
// 更新图表数据(核心方法)
updateCharts(data) {
// 1. 更新趋势图
if (this.charts.trend) {
this.charts.trend.setOption({
series: [
{ data: data.salesTrend.sales },
{ data: data.salesTrend.orders }
]
});
}
// 2. 更新品类图
if (this.charts.category) {
const pieData = data.category.names.map((name, idx) => ({
name: name,
value: data.category.values[idx]
}));
this.charts.category.setOption({ series: [{ data: pieData }] });
}
// 3. 更新区域图
if (this.charts.region) {
this.charts.region.setOption({ series: [{ data: data.region.sales }] });
}
// 4. 更新KPI指标(数字滚动效果)
this.updateKPI(data.kpi);
// 5. 更新实时数据
this.updateRealtimeData(data.kpi.onlineUsers);
// 控制台输出更新时间
console.log(`[${new Date(data.timestamp).toLocaleTimeString()}] 数据已更新`);
}
// 更新KPI指标
updateKPI(kpi) {
const kpiElements = {
totalSales: document.getElementById('totalSales'),
totalOrders: document.getElementById('totalOrders'),
conversion: document.getElementById('conversion'),
onlineUsers: document.getElementById('onlineUsers')
};
if (kpiElements.totalSales) {
this.animateNumber(kpiElements.totalSales, parseFloat(kpiElements.totalSales.textContent) || 0, kpi.totalSales);
}
if (kpiElements.totalOrders) {
this.animateNumber(kpiElements.totalOrders, parseInt(kpiElements.totalOrders.textContent) || 0, kpi.totalOrders);
}
if (kpiElements.conversion) {
kpiElements.conversion.textContent = kpi.conversion;
}
if (kpiElements.onlineUsers) {
this.animateNumber(kpiElements.onlineUsers, parseInt(kpiElements.onlineUsers.textContent) || 0, kpi.onlineUsers);
}
}
// 数字滚动动画
animateNumber(element, start, end, duration = 1000) {
const startTime = performance.now();
const update = (currentTime) => {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
// 缓动函数 easeOutQuad
const easeProgress = 1 - (1 - progress) * (1 - progress);
const current = start + (end - start) * easeProgress;
element.textContent = Math.floor(current).toLocaleString();
if (progress < 1) {
requestAnimationFrame(update);
} else {
element.textContent = end.toLocaleString();
}
};
requestAnimationFrame(update);
}
// 实时数据队列(用于存储最近30个数据点)
constructor() {
// ... 之前的内容
this.realtimeData = {
times: [],
values: []
};
this.maxDataPoints = 30; // 最多保存30个数据点
}
// 更新实时数据
updateRealtimeData(onlineUsers) {
const now = new Date();
const timeStr = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`;
// 添加新数据
this.realtimeData.times.push(timeStr);
this.realtimeData.values.push(onlineUsers);
// 保持数据点数量不超过最大值
if (this.realtimeData.times.length > this.maxDataPoints) {
this.realtimeData.times.shift();
this.realtimeData.values.shift();
}
// 更新图表
if (this.charts.realtime) {
this.charts.realtime.setOption({
xAxis: { data: this.realtimeData.times },
series: [{ data: this.realtimeData.values }]
});
}
}
// WebSocket连接
connectWebSocket() {
try {
this.ws = new WebSocket(WS_CONFIG.url);
// 连接打开事件
this.ws.onopen = (event) => {
console.log('WebSocket连接已建立');
this.isConnected = true;
this.reconnectAttempts = 0;
// 发送订阅消息(如果需要)
this.ws.send(JSON.stringify({
type: 'subscribe',
channel: 'dashboard',
action: 'subscribe'
}));
};
// 接收消息事件
this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this.updateCharts(data);
} catch (error) {
console.error('解析数据失败:', error);
}
};
// 连接关闭事件
this.ws.onclose = (event) => {
console.warn('WebSocket连接已关闭, 尝试重连...');
this.isConnected = false;
this.reconnect();
};
// 连接错误事件
this.ws.onerror = (error) => {
console.error('WebSocket错误:', error);
};
} catch (error) {
console.error('创建WebSocket连接失败:', error);
this.reconnect();
}
}
// 重连机制
reconnect() {
if (this.reconnectAttempts >= WS_CONFIG.maxReconnectAttempts) {
console.error('已达到最大重连次数, 停止重连');
// 切换到轮询模式
this.startPolling();
return;
}
this.reconnectAttempts++;
console.log(`第${this.reconnectAttempts}次重连, ${WS_CONFIG.reconnectInterval}ms后重试`);
setTimeout(() => {
this.connectWebSocket();
}, WS_CONFIG.reconnectInterval);
}
// 轮询模式(WebSocket失败时的降级方案)
startPolling() {
console.log('启动HTTP轮询模式, 每5秒获取一次数据');
const mockGen = new MockDataGenerator();
this.pollingInterval = setInterval(() => {
const mockData = mockGen.generateFullData();
this.updateCharts(mockData);
}, 5000);
}
// 发送消息
sendMessage(message) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(message));
} else {
console.warn('WebSocket未连接, 消息未发送');
}
}
// 停止所有连接
stop() {
if (this.ws) {
this.ws.close();
}
if (this.pollingInterval) {
clearInterval(this.pollingInterval);
}
}
}
// 初始化动态大屏
document.addEventListener('DOMContentLoaded', () => {
const manager = new DynamicChartManager();
manager.initCharts();
manager.connectWebSocket();
});
4.3 大屏展示效果增强
javascript
/**
* 大屏效果增强 - 动画和装饰
*/
class DashboardEnhancer {
constructor() {
this.initTimeUpdate(); // 时间更新
this.initBorderAnimation(); // 边框动画
this.initWatermark(); // 水印效果
}
// 实时时间更新
initTimeUpdate() {
const timeElement = document.getElementById('currentTime');
if (!timeElement) return;
const updateTime = () => {
const now = new Date();
const year = now.getFullYear();
const month = (now.getMonth() + 1).toString().padStart(2, '0');
const day = now.getDate().toString().padStart(2, '0');
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
const weekday = weekdays[now.getDay()];
const hour = now.getHours().toString().padStart(2, '0');
const minute = now.getMinutes().toString().padStart(2, '0');
const second = now.getSeconds().toString().padStart(2, '0');
timeElement.innerHTML = `${year}-${month}-${day} ${weekday} ${hour}:${minute}:${second}`;
};
updateTime();
setInterval(updateTime, 1000);
}
// 边框光效动画
initBorderAnimation() {
const style = document.createElement('style');
style.textContent = `
@keyframes borderPulse {
0% {
box-shadow: 0 0 0 0 rgba(64, 158, 255, 0);
border-color: rgba(64, 158, 255, 0.3);
}
50% {
box-shadow: 0 0 20px 2px rgba(64, 158, 255, 0.3);
border-color: rgba(64, 158, 255, 0.8);
}
100% {
box-shadow: 0 0 0 0 rgba(64, 158, 255, 0);
border-color: rgba(64, 158, 255, 0.3);
}
}
.chart-box {
animation: borderPulse 4s ease-in-out infinite;
}
/* 图表悬停放大效果 */
.chart-box:hover {
transform: scale(1.02);
transition: transform 0.3s ease;
z-index: 100;
}
/* 数据加载动画 */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.chart-box {
animation: fadeInUp 0.6s ease-out forwards;
opacity: 0;
}
/* 逐项延迟显示 */
.chart-box:nth-child(1) { animation-delay: 0.1s; }
.chart-box:nth-child(2) { animation-delay: 0.2s; }
.chart-box:nth-child(3) { animation-delay: 0.3s; }
.chart-box:nth-child(4) { animation-delay: 0.4s; }
.chart-box:nth-child(5) { animation-delay: 0.5s; }
`;
document.head.appendChild(style);
}
// 水印效果(可选)
initWatermark() {
const watermark = document.createElement('div');
watermark.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
color: rgba(255,255,255,0.1);
font-size: 12px;
pointer-events: none;
z-index: 9999;
`;
watermark.textContent = '数据大屏 | 实时监控系统 V1.0';
document.body.appendChild(watermark);
}
// 全屏切换功能
initFullscreen() {
const btn = document.getElementById('fullscreenBtn');
if (!btn) return;
btn.addEventListener('click', () => {
const element = document.documentElement;
if (document.fullscreenElement) {
document.exitFullscreen();
btn.textContent = '🖥️ 全屏';
} else {
element.requestFullscreen();
btn.textContent = '📴 退出全屏';
}
});
// 监听全屏变化事件
document.addEventListener('fullscreenchange', () => {
if (document.fullscreenElement) {
btn.textContent = '📴 退出全屏';
} else {
btn.textContent = '🖥️ 全屏';
}
});
}
}
// 初始化增强效果
const enhancer = new DashboardEnhancer();
enhancer.initFullscreen();
五、本章小结
核心知识点总结表
| 知识点 | 关键内容 | 代码要点 |
|---|---|---|
| ECharts初始化 | echarts.init(dom) |
需要DOM容器,支持自适应 |
| 图表配置项 | option对象 | 包含title、tooltip、legend、series等 |
| 图表类型 | line、bar、pie、gauge | 不同场景选择合适的类型 |
| 数据更新 | setOption(newData) |
支持增量更新和完全替换 |
| 动态数据 | WebSocket + 轮询 | 实现实时数据推送 |
| 动画效果 | 数字滚动 + 边框动画 | 提升用户体验 |
| 自适应 | resize() 事件监听 |
适配不同分辨率 |
| 全屏显示 | Fullscreen API | 大屏展示必要功能 |
完整项目代码结构
echarts-dashboard/
├── index.html # 主页面(包含所有样式和结构)
├── js/
│ ├── echarts.min.js # ECharts核心库
│ ├── data.js # 静态数据定义
│ ├── charts.js # 静态图表渲染
│ ├── dynamic.js # 动态图表管理
│ └── enhance.js # 效果增强和工具函数
└── README.md # 项目说明文档
常见问题FAQ
-
图表不显示?
- 检查DOM容器是否存在
- 确认ECharts库是否正确引入
- 查看浏览器控制台错误信息
-
图表尺寸不对?
- 调用
myChart.resize()方法 - 监听窗口resize事件
- 调用
-
WebSocket连接失败?
- 检查服务器地址是否正确
- 实现降级轮询方案
-
性能优化建议
- 使用
setOption时设置notMerge: false - 避免频繁创建新图表实例
- 合理设置更新频率
- 使用