基于 Vue3 + ECharts + Highcharts 打造银行级数据可视化大屏 —— 从设计到上线的完整实战
关键词:Vue3、ECharts、Highcharts、数据可视化大屏、响应式缩放、3D饼图、中国地图、组织架构图、Element Plus
前言:在金融科技领域,数据可视化大屏已成为审计管理、运营监控等核心场景的"数字驾驶舱"。本文将以**苏州银行统一审计平台驾驶舱**为例,从零拆解一个企业级数据可视化大屏的完整技术方案,涵盖**屏幕自适应、多图表混用、弹窗下钻、地图交互、3D可视化**等核心难点,提供可直接复用的代码模式和架构思路。

一、项目技术栈概览
| 技术 | 版本 | 用途 |
|------|------|------|
| Vue 3 | ^3.5.22 | 核心框架(Composition API + `<script setup>`) |
| Vite | ^7.1.11 | 构建工具(极快热更新) |
| TypeScript | ~5.9.0 | 类型安全 |
| ECharts | ^5.4.2 | 主力图表库(柱状图、折线图、散点图、地图) |
| Highcharts | ^11.4.0 | 3D饼图(人员分布) |
| Element Plus | ^2.12.0 | UI组件库(日期选择器、表格、弹窗、Popover) |
| Sass | ^1.94.1 | CSS预处理器 |
| Axios | ^1.13.2 | HTTP请求 |
| vue-router | ^4.6.3 | 路由管理 |
| postcss-pxtorem | ^6.1.0 | 移动端rem适配 |
项目结构
```
src/
├── api/ # 接口定义
│ └── auditScreen.ts # 大屏所有API(30+接口)
├── assets/ # 静态资源
│ ├── images/ # 图片资源(背景、图标、装饰)
│ └── font/ # 数字字体 DS-Digital
├── components/ # 组件库(28个业务组件)
│ ├── DashboardHeader.vue # 顶部标题栏 + 年份选择
│ ├── MetricCards.vue # 核心指标卡片区(Tab切换)
│ ├── BigScreenMiddle.vue # 中间区域容器(4个Tab页)
│ ├── TeamProjectStatus.vue # 各团队项目情况(堆叠柱状图)
│ ├── EconomicProjectStatus.vue # 经济责任项目情况
│ ├── SystemProjects.vue # 整体项目情况
│ ├── AuditResults.vue # 审计成果总量
│ ├── ResponsibilityStatus.vue # 责任认定情况
│ ├── StaffWorkload.vue # 审计人员工作量情况
│ ├── StaffTravelStatus.vue # 出差情况
│ ├── OrganizationalChart.vue # 组织架构图(CSS 3D动画)
│ ├── ECharts3DRing.vue # 3D饼图(Highcharts)
│ ├── Calendar.vue # 审计日历
│ ├── AuditMap.vue # 审计地图(中国地图+散点)
│ ├── PopupTable.vue # 弹窗表格通用组件
│ ├── DialogTable.vue # 弹窗表格组件
│ └── Title.vue # 卡片标题组件
├── utils/
│ ├── request.ts # Axios封装(拦截器、Token刷新)
│ └── status.ts # 错误码映射
├── views/
│ ├── auditScreen.vue # 主页面(三栏布局)
│ └── externalScreen.vue # 外部版页面
└── router/ # 路由配置
```
二、核心架构设计:三栏布局 + 自适应缩放
2.1 页面整体结构
大屏采用经典的**左-中-右三栏布局**,中间区域略宽以突出核心内容:
<!-- views/auditScreen.vue -->
<template>
<div class="dashboard-container" ref="wrapperRef">
<div class="screen-wrapper" :style="contentStyle">
<!-- 顶部标题栏 -->
<DashboardHeader :current-year="currentYear" @year-change="handleYearChange" />
<div class="main-box">
<div class="main-content">
<!-- 左栏:团队项目 / 经济责任 / 整体项目 -->
<div class="left-panel">
<TeamProjectStatus :currentYear="currentYear" />
<EconomicProjectStatus :currentYear="currentYear" />
<SystemProjects :currentYear="currentYear" />
</div>
<!-- 中栏:指标卡 / 组织架构/3D饼图/日历/地图 / 出差情况 -->
<div class="center-panel">
<MetricCards :timer-time="timerTime" :currentYear="currentYear" />
<BigScreenMiddle :currentYear="currentYear" :scale="scale" />
<StaffTravelStatus :currentYear="currentYear" />
</div>
<!-- 右栏:审计成果 / 责任认定 / 工作量 -->
<div class="right-panel">
<AuditResults :currentYear="currentYear" />
<ResponsibilityStatus :currentYear="currentYear" />
<StaffWorkload :currentYear="currentYear" />
</div>
</div>
</div>
</div>
</div>
</template>
2.2 等比缩放适配方案(关键!)
大屏开发最核心的难题是**不同分辨率下的适配**。本项目采用 `transform: scale()` 方案,基于 **1920×1080** 设计稿基准进行等比缩放:
// 核心缩放逻辑
const wrapperRef = ref<HTMLElement | null>(null)
const scale = ref(1)
const contentStyle = computed(() => ({
transform: `scale(${scale.value})`,
transformOrigin: '0 0',
width: `${100 / scale.value}%`,
height: `${100 / scale.value}%`,
}))
function updateScale() {
if (!wrapperRef.value) return
const baseWidth = 1920 // 设计稿基准宽度
const baseHeight = 1080 // 设计稿基准高度
const containerWidth = wrapperRef.value.clientWidth
const containerHeight = wrapperRef.value.clientHeight
// 选择较小值确保内容完整显示,不溢出
const scaleX = containerWidth / baseWidth
const scaleY = containerHeight / baseHeight
scale.value = Math.min(scaleX, scaleY)
}
onMounted(() => {
updateScale()
window.addEventListener('resize', updateScale)
})
onUnmounted(() => {
window.removeEventListener('resize', updateScale)
})
**为什么选择 scale 而不是 rem/vw?**
|
方案 |
优点 |
缺点 |
适用场景 |
|
scale(本项目) |
零改代码、完美还原设计稿、等比不变形 |
可能出现留白 |
固定比例大屏 |
|
rem/vw |
流式布局无留白 |
字体/间距需逐个调校、非等比 |
移动端/流式页面 |
|
百分比 |
简单直接 |
复杂布局难以控制 |
简单页面 |
三、核心技术点深度解析
3.1 多图表引擎混用:ECharts + Highcharts
本项目同时使用了两个图表库,各有分工:
# ECharts — 承担主力图表
// 高清屏优化初始化(所有ECharts图表统一模式)
import { markRaw } from 'vue'
const devicePixelRatio = Number(import.meta.env.VITE_APP_DEVICEPIXELRATIO) || 3
chart.value = markRaw(
echarts.init(chartRef.value, '', {
renderer: 'canvas',
devicePixelRatio: devicePixelRatio, // 适配高清屏(2K/4K)
useDirtyRect: false, // 保证渲染质量
}),
)
**关键实践要点**:
- 使用 `markRaw()` 包裹图表实例,避免 Vue 3 的深层响应式代理导致性能问题
- `devicePixelRatio` 设为 3 适配 Retina/高 DPI 屏幕
- 所有图表统一通过 `ResizeObserver` + `window.resize` 监听尺寸变化
# Highcharts — 3D饼图专属
import Highcharts from 'highcharts'
import Highcharts3D from 'highcharts/highcharts-3d'
Highcharts3D(Highcharts) // 启用3D模块
const chartOptions: Options = {
chart: {
type: 'pie',
backgroundColor: 'transparent',
options3d: {
enabled: true,
alpha: 70, // 倾斜角度
depth: 50, // 饼图厚度
},
},
plotOptions: {
pie: {
innerSize: '50%', // 环形
depth: 50,
dataLabels: {
useHTML: true,
format: `<div class="labels">
<img src="${personnelIcon}" />
<div>{point.name} {point.y}人</div>
</div>`,
},
},
},
}
**为什么用 Highcharts 做 3D 饼图?**
- ECharts 的 3D 支持相对有限,Highcharts 的 `highcharts-3d` 插件更成熟
- 倾斜角度(alpha)、厚度(depth)可精细调控
- 数据标签支持 HTML 模板,自定义程度高
3.2 中国地图 + 散点标注 + 弹窗下钻
审计地图是整个大屏交互最复杂的模块,技术要点包括:
**1)注册自定义地图 + 多层散点叠加**
// 注册中国地图GeoJSON
import chinaGeo from '../assets/images/china.json'
echarts.registerMap('china', geoJsonData)
// 四层series叠加:
// Layer 1: 地图底图(geo)
// Layer 2: map系列(省份填充色)
// Layer 3: 散点层 - 三角形标记(城市位置)
// Layer 4: 散点层 - 城市名称标签
// Layer 5: 散点层 - 人数统计标签
const option = {
geo: {
map: 'china',
aspectRatio: 0.9,
zoom: 1.21,
roam: false,
itemStyle: {
areaColor: '#0D1AC2',
shadowColor: '#0D1AC2',
shadowOffsetX: 7,
shadowOffsetY: 7,
},
},
series: [
// 地图填充
{ type: 'map', map: 'china', ... },
// 三角形标记(选中=绿色,未选=蓝色)
{
type: 'scatter',
coordinateSystem: 'geo',
symbol: (params) => params.name === selectedCity.value
? `image://${greenTriangle}`
: `image://${blueTriangle}`,
},
// 城市名称标签
{
type: 'scatter',
coordinateSystem: 'geo',
symbol: `image://${labelBg}`,
label: { formatter: (p) => `{city| ${p.name} }` },
},
// 人数标签
{
type: 'scatter',
coordinateSystem: 'geo',
symbolSize: (params) => [String(params.data.count).length * 10 + 14, 18],
label: { formatter: (p) => `{count| ${p.data.count}人 }` },
},
],
}
**2)点击交互 + 弹窗联动**
chart.value.on('click', (params) => {
if (params.seriesType === 'scatter') {
// 切换选中状态
selectedCity.value = selectedCity.value === params.name ? '' : params.name
updateChart() // 重新渲染(切换颜色)
if (selectedCity.value && params.data.count !== 0) {
// 定位弹窗位置(根据城市位置智能判断左右弹出方向)
popupTop.value = params.event.offsetY
popupLeft.value = getPopupLeft(params.name, params.event.offsetX)
getTeamData(params.name) // 加载该城市打卡详情
}
}
})
**3)弹窗内嵌套 Popover(二次下钻)**
地图点击 → 弹出城市信息卡片 → 点击"外勤人数" → 再次弹出人员明细表格。这种**多层下钻**模式通过 Element Plus 的 Popover + 可拖拽 resize 实现。
3.3 CSS 3D 组织架构动画
中间区域的组织架构图是纯 CSS 实现的 3D 椭圆轨道动画,无需任何图表库:
<!-- OrganizationalChart.vue 核心结构 -->
<div class="css-swiper">
<!-- 顶层:总审计师 -->
<div class="loop-top">
<div class="loop-run1"><!-- 总审计师 --></div>
<div class="loop-run2"><!-- 总经理室 --></div>
</div>
<!-- 底层:椭圆轨道旋转(10个团队环绕) -->
<div class="loop-bottom">
<div class="ellipse">
<div
v-for="(item, index) in list3"
class="xz-circle"
:style="item.style"
>
{{ item.name }}
</div>
</div>
</div>
</div>
```
```scss
.ellipse {
border-radius: 50%;
position: absolute;
transform-style: preserve-3d;
transform: rotateZ(90deg) rotateY(104deg); // 关键:3D倾斜变换
}
// 动态生成每个元素的轨道动画
@keyframes move${index} {
0% {
transform: rotateZ(360deg) translateX(12vw) rotateZ(-360deg) rotateY(-70deg);
}
100% {
transform: rotateZ(0deg) translateX(12vw) rotateZ(0deg) rotateY(-70deg);
}
}
**技术亮点**:
- `transform-style: preserve-3d` 开启 3D 渲染空间
- `rotateY(104deg)` 将圆形轨道压扁为椭圆视觉
- 动态计算 `animation-delay` 让元素均匀分布在轨道上
- 通过 `z-index` 动态变化实现前后遮挡关系
3.4 Tab 自动轮播 + 手动干预
多个模块都实现了 Tab 自动轮播(如 MetricCards 的部门切换、BigScreenMiddle 的中间区域切换),且支持鼠标悬停暂停:
// 通用轮播模式
const timer = ref<number | null>(null)
const timerTime = 10000 // 10秒间隔
const startTimer = () => {
stopTimer()
timer.value = setInterval(() => {
const currentIndex = tabs.findIndex(tab => tab.name === currentTab.value)
const nextIndex = (currentIndex + 1) % tabs.length
tabClickHandle(tabs[nextIndex])
}, props.timerTime)
}
const stopTimer = () => {
if (timer.value) {
clearInterval(timer.value)
timer.value = null
}
}
// 模板绑定
<div @mouseenter="stopTimer" @mouseleave="startTimer">
3.5 审计日历(基于 Element Plus El-Calendar 二次封装)
<el-calendar v-model="currentDate">
<template #date-cell="{ data }">
<el-popover placement="top" trigger="click">
<!-- 点击日期弹出当日出差人员明细 -->
<PopupTable :popupData="popupData" :column="column" />
<template #reference>
<div class="div-Calendar" @click="clickCalendar(data)">
<div class="date-number">{{ day }}</div>
<div class="sub-number">{{ taskCount }}</div> <!-- 当日任务数角标 -->
</div>
</template>
</el-popover>
</template>
</el-calendar>
**定制化改造**:
- 隐藏默认头部,自定义为深色风格的年月选择器
- 星期标题改为"周一、周二...周日"
- 日期单元格支持角标显示任务数量
- 点击日期弹出当日审计安排明细表
四、工程化实践
4.1 Axios 封装:Token 自动刷新
// utils/request.ts 核心逻辑
axios.interceptors.response.use(
async (response) => {
const code = response.data.code || 200
if (code === 401) {
// Token过期 → 尝试刷新
if (!isRefreshToken) {
isRefreshToken = true
try {
const newToken = await getRefreshToken()
// 保存新Token
localStorage.setItem(AccessTokenKey, newToken.accessToken)
// 重放队列中等待的请求
requestList.forEach(cb => cb())
return axios(response.config) // 重发当前请求
} catch (e) {
return handleAuthorized() // 刷新失败→登出
}
} else {
// 正在刷新中 → 排队等待
return new Promise(resolve => {
requestList.push(() => {
response.config.headers[AUTHORIZATION_ALIAS] = 'Bearer ' + token()
resolve(axios(response.config))
})
})
}
} else if (code === 1) {
return response.data // 业务成功码
}
}
)
这套机制解决了**并发请求时多次刷新Token**的经典问题:
- 第一个401触发刷新,后续401进入队列
- 刷新成功后批量重放
- 刷新失败统一登出
4.2 Vite 配置:自动导入 + rem适配
// vite.config.ts
export default defineConfig({
plugins: [
vue(),
AutoImport({ resolvers: [ElementPlusResolver()] }), // API自动导入
Components({ resolvers: [ElementPlusResolver()] }), // 组件自动注册
],
css: {
postcss: {
plugins: [
postcsspxtorem({
rootValue: 16, // 1rem = 16px
propList: ['*'], // 所有属性转换
selectorBlackList: ['.norem'], // 排除特定选择器
minPixelValue: 2, // 小于2px的不转换
}),
],
},
},
})
4.3 组件通信模式
|
场景 |
方式 |
说明 |
|
年份变更影响全局 |
`props` 单向传递 |
currentYear 从顶层传入每个子组件 |
|
缩放样式传递给弹窗 |
`provide/inject` |
contentStyle 注入到所有子孙弹窗组件 |
|
弹窗关闭通知父组件 |
`emit` |
moreClick、year-change 等 |
|
权限控制 |
API获取后props分发 |
isMore 控制各模块是否显示"更多"按钮和弹窗 |
五、视觉与交互细节
大屏采用统一的**深蓝色科技风**配色体系:
// 主背景色
background: #000b25;
// 卡片渐变边框(模拟发光效果)
.card-wrap {
background: linear-gradient(270deg, #000768 0%, #5058c2 51%, #000768 100%);
&::after {
// 右下角光晕效果
background:
linear-gradient(to right, transparent 70%, rgba(0, 180, 255, 0.4) 95%),
linear-gradient(to bottom, transparent 70%, rgba(100, 80, 255, 0.4) 95%);
}
}
// 卡片内部
.card-body {
background-color: #012549;
border: 1px solid #304f69;
box-shadow: inset 0 0 10px rgba(0, 200, 255, 0.2);
}
// 强调色
$primary-cyan: #34fefe; // 数值高亮
$primary-green: #aeff58; // 次要数值
$accent-blue: #009bff; // 文字/按钮
$glow-color: rgba(0, 217, 255, 0.6); // 发光效果
5.2 数字字体应用
核心指标数值使用 **DS-Digital-BoldItalic** 等宽字体,增强数据可读性:
@font-face {
font-family: 'DS-Digital-BoldItalic';
src: url('@/assets/font/DS-Digital-Bold-Italic/DS-Digital-Bold-Italic.ttf');
}
.card-value {
font-size: 32px;
color: #34fefe;
font-family: 'DS-Digital-BoldItalic' !important;
}
5.3 通用卡片容器模式
所有数据卡片遵循统一的结构模式:
```
Title(标题 + 更多按钮)
└─ card-wrap(渐变边框外壳)
└─ card-body(内容区 + 内阴影边框)
└─ 图表 / 表格 / 其他内容
```
每个卡片都有:
- 入场动画 `fadeIn`(上滑 + 渐显)
- hover 时微交互(上移 + 阴影发光)
- 统一的圆角、内边距、边框样式
六、性能优化策略
6.1 图表性能
|
优化手段 |
说明 |
|
markRaw() |
避免 Vue 代理 ECharts 实例 |
|
ResizeObserver |
替代 resize 事件,精确监听容器变化 |
|
useDirtyRect: false |
牺牲部分性能换取渲染质量(大屏场景优先保真) |
|
devicePixelRatio: 3 |
适配 4K/Retina 屏幕 |
|
按需销毁 |
`onUnmounted` 中调用 `dispose()` / `destroy()` |
七、常见问题与解决方案
# Q1:大屏在不同比例显示器上有黑边怎么办?
这是 `scale` 方案的固有特性。如果需要全屏铺满,可以改为 `scaleX = scaleX, scaleY = scaleY` 分别缩放(会轻微变形),或者让背景图/背景色延伸填满。
# Q2:ECharts 在弹窗中尺寸不对?
弹窗初始隐藏时容器宽度为 0,需要在弹窗打开后 `nextTick` 中调用 `chart.resize()`。本项目通过 `ResizeObserver` 自动处理此问题。
# Q3:地图点击事件不准确?
散点图的 click 事件坐标可能与视觉位置有偏差,建议使用 `event.offsetX/Y` 而非 `event.clientX/Y` 来定位弹窗。
# Q4:Highcharts 和 ECharts 共存有冲突吗?
没有冲突。两者各自管理自己的实例和命名空间,只要注意:
- 分别 `dispose()` / `destroy()`
- 不要共享同一个 DOM 容器
- 全局样式做好命名空间隔离(`:deep()` 限定作用域)
八、总结
本文完整拆解了一个**银行级数据可视化大屏**的技术实现方案,核心要点回顾:
1. **`transform: scale()` 等比缩放**是大屏适配的最优解,零改业务代码
2. **ECharts + Highcharts 混用**发挥各自优势,ECharts 扛主力、Highcharts 补 3D
3. **纯 CSS 3D 动画**可以实现复杂的组织架构旋转效果,无需引入额外依赖
4. **多层下钻交互**(图表→弹窗→Popover→表格)是提升数据探索效率的关键
5. **统一的暗色设计系统和卡片模式**保证视觉一致性
6. **Axios Token 无感刷新**保障长时间运行大屏的认证连续性
更多推荐

所有评论(0)