关键词: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 无感刷新**保障长时间运行大屏的认证连续性

更多推荐