前端数据大屏开发完整指南:Vue3 + ECharts 自适应可视化实战

前端数据大屏开发完整指南:Vue3 + ECharts + 自适应 + 接口 + 部署

本文是一份完整的数据大屏开发指南,覆盖:大屏开发思路、技术栈选择、项目结构、1920×1080 自适应方案、Vue3 + ECharts 完整可运行示例、图表封装、地图开发、接口轮询、WebSocket 实时数据、视觉设计、性能优化、部署上线、学习资源与官方文档。


目录

  1. 数据大屏是什么
  2. 大屏开发适合哪些场景
  3. 大屏开发常用技术栈
  4. 大屏开发整体流程
  5. 项目目录结构推荐
  6. 大屏布局方案
  7. 大屏自适应详细讲解
  8. [Vue3 + ECharts 完整可运行示例](#Vue3 + ECharts 完整可运行示例)
  9. [ECharts 图表封装与使用](#ECharts 图表封装与使用)
  10. 地图大屏开发
  11. 接口数据接入
  12. [WebSocket 实时数据推送](#WebSocket 实时数据推送)
  13. 大屏视觉设计要点
  14. 全屏展示处理
  15. 性能优化重点
  16. 部署上线
  17. 常见问题与解决方案
  18. 学习资源与官方文档
  19. 大屏开发总结

一、数据大屏是什么

数据大屏,也叫数据可视化大屏、运营驾驶舱、智慧大屏,是一种通过图表、地图、数字指标、排行榜、动画效果等方式,把业务数据集中展示在大屏幕上的前端页面。

它和普通后台管理系统最大的区别是:

对比项 普通后台系统 数据大屏
页面目标 操作、管理、录入、查询 展示、监控、汇报、分析
页面尺寸 响应式为主 通常固定设计稿尺寸
交互方式 表单、表格、按钮较多 图表、地图、指标展示较多
滚动方式 可以上下滚动 一般不允许滚动
视觉风格 简洁实用 科技感、炫酷感、可视化
数据刷新 用户主动操作较多 自动刷新、实时推送较多
常见尺寸 PC、移动端、平板 1920×1080、2K、4K、拼接屏

数据大屏的核心是:

txt 复制代码
把复杂的数据,用直观、清晰、稳定、好看的方式展示出来。

二、大屏开发适合哪些场景

常见应用场景:

txt 复制代码
1. 智慧城市大屏
2. 企业运营驾驶舱
3. 销售数据大屏
4. 电商订单大屏
5. 医院数据大屏
6. 物流运输大屏
7. 工厂生产监控大屏
8. 安防监控大屏
9. 能源数据大屏
10. 交通数据大屏
11. 机房监控大屏
12. 网站流量统计大屏
13. 活动现场数据展示大屏
14. 展厅可视化大屏
15. 政务数据展示大屏

三、大屏开发常用技术栈

1. 推荐技术栈

比较推荐前端使用:

txt 复制代码
Vue3 + TypeScript + Vite
ECharts 图表
Axios 请求接口
WebSocket 实时数据
Pinia 状态管理
CSS Grid / Flex 布局
transform scale 自适应
Nginx 部署

2. 技术栈说明

技术 作用
Vue3 页面组件化开发
TypeScript 增强代码类型安全
Vite 快速构建和开发服务
ECharts 绘制柱状图、折线图、饼图、地图、飞线图
Axios 请求后端接口
WebSocket 实时推送数据
Pinia 管理全局状态
CSS Grid 大屏整体布局
Flex 局部模块布局
ResizeObserver 监听图表容器尺寸变化
Nginx 静态资源部署和接口代理

3. 可选技术

技术 适合场景
DataV 边框、装饰、数字翻牌、水位图等
Three.js 3D 地球、3D 地图、3D 场景
D3.js 高度自定义可视化
Mapbox / MapLibre 高级地图可视化
高德地图 JS API 真实地图业务场景
百度地图 API 国内地图业务场景
GSAP 高级动画
Lottie 设计师导出的动画效果

四、大屏开发整体流程

一个完整的大屏项目通常按下面流程开发:

txt 复制代码
第一步:明确业务需求
第二步:确定设计稿尺寸,比如 1920×1080
第三步:确定数据来源和接口字段
第四步:搭建 Vue3 + Vite 项目
第五步:实现大屏自适应容器
第六步:拆分 Header、Panel、Chart、Map、Card 组件
第七步:封装 ECharts 通用图表组件
第八步:开发图表、地图、指标卡、排行榜
第九步:接入后端接口数据
第十步:实现定时刷新或 WebSocket 实时推送
第十一步:优化动画、图表和性能
第十二步:打包部署到服务器
第十三步:在真实大屏设备上测试

五、项目目录结构推荐

推荐目录结构:

txt 复制代码
src
├── api
│   └── screen.ts              # 大屏接口请求
├── assets
│   ├── images                 # 背景图、装饰图
│   └── styles
│       ├── reset.css
│       └── screen.css
├── components
│   ├── charts
│   │   ├── BaseChart.vue      # ECharts 通用组件
│   │   ├── BarChart.vue
│   │   ├── LineChart.vue
│   │   ├── PieChart.vue
│   │   └── MapChart.vue
│   ├── cards
│   │   └── NumberCard.vue     # 指标卡
│   ├── layout
│   │   ├── ScaleScreen.vue    # 大屏缩放容器
│   │   └── ScreenPanel.vue    # 面板组件
│   └── ranking
│       └── RankList.vue
├── composables
│   ├── useScreenScale.ts      # 大屏自适应
│   ├── useEcharts.ts          # ECharts 逻辑封装
│   ├── usePolling.ts          # 定时轮询
│   └── useWebSocket.ts        # WebSocket 封装
├── stores
│   └── screen.ts              # 大屏状态管理
├── views
│   └── BigScreen.vue          # 大屏页面
├── router
├── main.ts
└── App.vue

六、大屏布局方案

1. 常见布局一:三栏布局

适合智慧城市、运营驾驶舱、企业数据大屏。

txt 复制代码
顶部标题栏
左侧图表区 | 中间地图/核心业务区 | 右侧图表区
底部趋势/明细区

CSS 示例:

css 复制代码
.screen-main {
  height: calc(100% - 90px);
  padding: 20px;
  display: grid;
  grid-template-columns: 460px 1fr 460px;
  grid-template-rows: 1fr 260px;
  gap: 20px;
}

.left-panel {
  grid-row: 1 / 3;
}

.center-map {
  grid-column: 2 / 3;
}

.right-panel {
  grid-row: 1 / 3;
}

.bottom-chart {
  grid-column: 2 / 3;
}

2. 常见布局二:九宫格布局

适合指标卡较多、图表较多的业务看板。

css 复制代码
.dashboard-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-auto-rows: 300px;
  gap: 20px;
}

3. 常见布局三:中间地图 + 两侧图表

这是最典型的大屏布局。

txt 复制代码
┌──────────────────────────────┐
│            顶部标题           │
├────────┬────────────┬────────┤
│ 左图表 │   中间地图   │ 右图表 │
│ 左图表 │   核心指标   │ 右图表 │
│ 左图表 │   趋势图     │ 右图表 │
└────────┴────────────┴────────┘

七、大屏自适应详细讲解

大屏自适应是开发数据大屏最重要的一块。

1. 大屏自适应要解决什么

普通网页自适应关注的是:

txt 复制代码
PC、平板、手机都能浏览
内容可以自动换行
页面可以上下滚动

数据大屏自适应关注的是:

txt 复制代码
设计稿完整展示
页面不能出现滚动条
图表不能错位
文字、地图、边框、动画比例一致
适配 1920×1080、2K、4K、投影屏、拼接屏

所以数据大屏通常不采用普通响应式页面的开发方式,而是采用:

txt 复制代码
固定设计稿尺寸 + 整体等比缩放

2. 常见自适应方案对比

方案 原理 优点 缺点 推荐程度
transform: scale() 固定设计稿宽高,整体等比缩放 最稳定,最适合大屏 需要处理居中和留白 强烈推荐
vw / vh 根据视口宽高计算尺寸 写起来简单 字体、图表、边框容易比例不统一 一般
rem 动态修改根字体大小 适合移动端 大屏元素很多时维护成本高 一般
百分比布局 容器按比例拉伸 适合后台页面 很难保证设计稿还原 不推荐单独使用
zoom 浏览器缩放 简单 兼容性和标准性较差 不推荐
非等比缩放 宽高分别缩放 能铺满屏幕 页面会被拉伸变形 特殊场景可用

3. 推荐方案:固定 1920×1080 + 等比缩放

假设设计稿尺寸:

ts 复制代码
const designWidth = 1920
const designHeight = 1080

浏览器当前尺寸:

ts 复制代码
const viewportWidth = window.innerWidth
const viewportHeight = window.innerHeight

计算缩放比例:

ts 复制代码
const scale = Math.min(
  viewportWidth / designWidth,
  viewportHeight / designHeight
)

为什么使用 Math.min

因为要保证整个大屏完整显示,不被裁剪。

例如:

txt 复制代码
设计稿:1920 × 1080

屏幕一:1920 × 1080
scale = min(1920 / 1920, 1080 / 1080)
scale = 1

屏幕二:1366 × 768
scale = min(1366 / 1920, 768 / 1080)
scale = 0.711

屏幕三:2560 × 1440
scale = min(2560 / 1920, 1440 / 1080)
scale = 1.333

屏幕四:3840 × 2160
scale = min(3840 / 1920, 2160 / 1080)
scale = 2

4. 居中偏移计算

缩放后的真实宽高:

ts 复制代码
const realWidth = designWidth * scale
const realHeight = designHeight * scale

计算居中偏移量:

ts 复制代码
const offsetX = (window.innerWidth - realWidth) / 2
const offsetY = (window.innerHeight - realHeight) / 2

最终样式:

css 复制代码
transform-origin: left top;
transform: translate(offsetX, offsetY) scale(scale);

完整公式:

ts 复制代码
const scaleX = window.innerWidth / designWidth
const scaleY = window.innerHeight / designHeight
const scale = Math.min(scaleX, scaleY)

const realWidth = designWidth * scale
const realHeight = designHeight * scale

const offsetX = (window.innerWidth - realWidth) / 2
const offsetY = (window.innerHeight - realHeight) / 2

5. 为什么会有黑边

如果屏幕比例和设计稿比例不一致,就会出现黑边。

例如:

txt 复制代码
设计稿:1920×1080,比例 16:9
屏幕:1440×900,比例 16:10

为了保持页面不变形,就必须完整等比缩放。

这时上下或左右可能出现留白。

这是正常现象。

常见处理方式:

方案 说明 推荐
内容等比缩放,背景铺满 内容不变形,视觉更完整 推荐
内容等比缩放,允许黑边 最稳定 推荐
内容宽高分别缩放 强制铺满,但会变形 不推荐

6. 自适应核心 Hook:useScreenScale.ts

ts 复制代码
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'

interface UseScreenScaleOptions {
  width?: number
  height?: number
  minScale?: number
  maxScale?: number
}

export function useScreenScale(options: UseScreenScaleOptions = {}) {
  const designWidth = options.width ?? 1920
  const designHeight = options.height ?? 1080
  const minScale = options.minScale ?? 0
  const maxScale = options.maxScale ?? Infinity

  const scale = ref(1)
  const offsetX = ref(0)
  const offsetY = ref(0)
  const viewportWidth = ref(0)
  const viewportHeight = ref(0)

  let rafId = 0

  const calcScale = () => {
    viewportWidth.value = window.innerWidth
    viewportHeight.value = window.innerHeight

    const scaleX = viewportWidth.value / designWidth
    const scaleY = viewportHeight.value / designHeight

    let nextScale = Math.min(scaleX, scaleY)

    nextScale = Math.max(minScale, Math.min(nextScale, maxScale))

    const realWidth = designWidth * nextScale
    const realHeight = designHeight * nextScale

    scale.value = nextScale
    offsetX.value = (viewportWidth.value - realWidth) / 2
    offsetY.value = (viewportHeight.value - realHeight) / 2
  }

  const resize = () => {
    cancelAnimationFrame(rafId)

    rafId = requestAnimationFrame(() => {
      calcScale()
    })
  }

  const screenStyle = computed(() => ({
    width: `${designWidth}px`,
    height: `${designHeight}px`,
    transform: `translate(${offsetX.value}px, ${offsetY.value}px) scale(${scale.value})`,
    transformOrigin: 'left top'
  }))

  onMounted(async () => {
    await nextTick()
    calcScale()
    window.addEventListener('resize', resize)
  })

  onUnmounted(() => {
    cancelAnimationFrame(rafId)
    window.removeEventListener('resize', resize)
  })

  return {
    designWidth,
    designHeight,
    scale,
    offsetX,
    offsetY,
    viewportWidth,
    viewportHeight,
    screenStyle,
    resize
  }
}

7. 自适应容器组件:ScaleScreen.vue

vue 复制代码
<template>
  <div class="scale-screen-wrapper">
    <div class="scale-screen-container" :style="screenStyle">
      <slot />
    </div>
  </div>
</template>

<script setup lang="ts">
import { useScreenScale } from '../composables/useScreenScale'

const props = withDefaults(
  defineProps<{
    width?: number
    height?: number
    minScale?: number
    maxScale?: number
  }>(),
  {
    width: 1920,
    height: 1080,
    minScale: 0,
    maxScale: Infinity
  }
)

const { screenStyle } = useScreenScale({
  width: props.width,
  height: props.height,
  minScale: props.minScale,
  maxScale: props.maxScale
})
</script>

<style scoped>
.scale-screen-wrapper {
  position: fixed;
  inset: 0;
  overflow: hidden;
  background:
    radial-gradient(circle at center, rgba(28, 92, 170, 0.45), transparent 45%),
    linear-gradient(135deg, #020817 0%, #061932 55%, #020817 100%);
}

.scale-screen-container {
  position: absolute;
  left: 0;
  top: 0;
  overflow: hidden;
}
</style>

使用方式:

vue 复制代码
<template>
  <ScaleScreen :width="1920" :height="1080">
    <BigScreenContent />
  </ScaleScreen>
</template>

八、Vue3 + ECharts 完整可运行示例

下面示例可以直接运行,包含:

txt 复制代码
Vue3
TypeScript
Vite
ECharts
大屏自适应
图表 resize
指标卡
排行榜
模拟地图区域
全屏按钮

1. 创建项目

bash 复制代码
npm create vite@latest big-screen-demo -- --template vue-ts

cd big-screen-demo

npm install

npm install echarts

npm run dev

2. 目录结构

txt 复制代码
big-screen-demo
├── package.json
├── index.html
└── src
    ├── main.ts
    ├── style.css
    ├── App.vue
    ├── composables
    │   └── useScreenScale.ts
    └── components
        ├── ScaleScreen.vue
        └── BaseChart.vue

3. src/main.ts

ts 复制代码
import { createApp } from 'vue'
import App from './App.vue'
import './style.css'

createApp(App).mount('#app')

4. src/style.css

css 复制代码
* {
  box-sizing: border-box;
}

html,
body,
#app {
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
}

body {
  overflow: hidden;
  font-family:
    Arial,
    "Microsoft YaHei",
    "PingFang SC",
    sans-serif;
  background: #020817;
}

button {
  font-family: inherit;
}

5. src/composables/useScreenScale.ts

ts 复制代码
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'

interface UseScreenScaleOptions {
  width?: number
  height?: number
  minScale?: number
  maxScale?: number
}

export function useScreenScale(options: UseScreenScaleOptions = {}) {
  const designWidth = options.width ?? 1920
  const designHeight = options.height ?? 1080
  const minScale = options.minScale ?? 0
  const maxScale = options.maxScale ?? Infinity

  const scale = ref(1)
  const offsetX = ref(0)
  const offsetY = ref(0)
  const viewportWidth = ref(0)
  const viewportHeight = ref(0)

  let rafId = 0

  const calcScale = () => {
    viewportWidth.value = window.innerWidth
    viewportHeight.value = window.innerHeight

    const scaleX = viewportWidth.value / designWidth
    const scaleY = viewportHeight.value / designHeight

    let nextScale = Math.min(scaleX, scaleY)

    nextScale = Math.max(minScale, Math.min(nextScale, maxScale))

    const realWidth = designWidth * nextScale
    const realHeight = designHeight * nextScale

    scale.value = nextScale
    offsetX.value = (viewportWidth.value - realWidth) / 2
    offsetY.value = (viewportHeight.value - realHeight) / 2
  }

  const resize = () => {
    cancelAnimationFrame(rafId)
    rafId = requestAnimationFrame(calcScale)
  }

  const screenStyle = computed(() => ({
    width: `${designWidth}px`,
    height: `${designHeight}px`,
    transform: `translate(${offsetX.value}px, ${offsetY.value}px) scale(${scale.value})`,
    transformOrigin: 'left top'
  }))

  onMounted(async () => {
    await nextTick()
    calcScale()
    window.addEventListener('resize', resize)
  })

  onUnmounted(() => {
    cancelAnimationFrame(rafId)
    window.removeEventListener('resize', resize)
  })

  return {
    scale,
    offsetX,
    offsetY,
    viewportWidth,
    viewportHeight,
    screenStyle,
    resize
  }
}

6. src/components/ScaleScreen.vue

vue 复制代码
<template>
  <div class="scale-screen-wrapper">
    <div class="scale-screen-container" :style="screenStyle">
      <slot />
    </div>
  </div>
</template>

<script setup lang="ts">
import { useScreenScale } from '../composables/useScreenScale'

const props = withDefaults(
  defineProps<{
    width?: number
    height?: number
    minScale?: number
    maxScale?: number
  }>(),
  {
    width: 1920,
    height: 1080,
    minScale: 0,
    maxScale: Infinity
  }
)

const { screenStyle } = useScreenScale({
  width: props.width,
  height: props.height,
  minScale: props.minScale,
  maxScale: props.maxScale
})
</script>

<style scoped>
.scale-screen-wrapper {
  position: fixed;
  inset: 0;
  overflow: hidden;
  background:
    radial-gradient(circle at center, rgba(28, 92, 170, 0.45), transparent 45%),
    linear-gradient(135deg, #020817 0%, #061932 55%, #020817 100%);
}

.scale-screen-container {
  position: absolute;
  left: 0;
  top: 0;
  overflow: hidden;
}
</style>

7. src/components/BaseChart.vue

vue 复制代码
<template>
  <div ref="chartRef" class="base-chart"></div>
</template>

<script setup lang="ts">
import * as echarts from 'echarts'
import type { ECharts, EChartsOption } from 'echarts'
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'

const props = defineProps<{
  option: EChartsOption
}>()

const chartRef = ref<HTMLDivElement | null>(null)

let chartInstance: ECharts | null = null
let resizeObserver: ResizeObserver | null = null

const initChart = async () => {
  await nextTick()

  if (!chartRef.value) return

  chartInstance = echarts.init(chartRef.value)
  chartInstance.setOption(props.option)

  resizeObserver = new ResizeObserver(() => {
    chartInstance?.resize()
  })

  resizeObserver.observe(chartRef.value)
}

watch(
  () => props.option,
  newOption => {
    chartInstance?.setOption(newOption, true)
  },
  {
    deep: true
  }
)

onMounted(() => {
  initChart()
})

onBeforeUnmount(() => {
  resizeObserver?.disconnect()
  chartInstance?.dispose()

  resizeObserver = null
  chartInstance = null
})
</script>

<style scoped>
.base-chart {
  width: 100%;
  height: 100%;
}
</style>

8. src/App.vue

vue 复制代码
<template>
  <ScaleScreen :width="1920" :height="1080">
    <div class="big-screen">
      <header class="screen-header">
        <div class="header-left">数据更新时间:{{ currentTime }}</div>
        <h1>智慧运营数据可视化大屏</h1>
        <button class="fullscreen-btn" @click="toggleFullScreen">
          {{ isFullScreen ? '退出全屏' : '全屏展示' }}
        </button>
      </header>

      <main class="screen-main">
        <section class="left-area">
          <div class="panel">
            <div class="panel-title">访问来源统计</div>
            <div class="chart-box">
              <BaseChart :option="barOption" />
            </div>
          </div>

          <div class="panel">
            <div class="panel-title">业务增长趋势</div>
            <div class="chart-box">
              <BaseChart :option="lineOption" />
            </div>
          </div>
        </section>

        <section class="center-area">
          <div class="number-grid">
            <div
              v-for="item in numberCards"
              :key="item.label"
              class="number-card"
            >
              <div class="number-label">{{ item.label }}</div>
              <div class="number-value">
                {{ item.value }}
                <span>{{ item.unit }}</span>
              </div>
            </div>
          </div>

          <div class="map-panel">
            <div class="map-title">核心业务区域</div>
            <div class="map-content">
              <div class="map-circle circle-1"></div>
              <div class="map-circle circle-2"></div>
              <div class="map-circle circle-3"></div>

              <div class="city city-gz">广州</div>
              <div class="city city-sz">深圳</div>
              <div class="city city-xm">厦门</div>
              <div class="city city-gy">贵阳</div>

              <div class="map-center">业务中心</div>
            </div>
          </div>

          <div class="panel center-bottom-panel">
            <div class="panel-title">实时订单趋势</div>
            <div class="chart-box bottom-chart">
              <BaseChart :option="areaLineOption" />
            </div>
          </div>
        </section>

        <section class="right-area">
          <div class="panel">
            <div class="panel-title">用户类型占比</div>
            <div class="chart-box">
              <BaseChart :option="pieOption" />
            </div>
          </div>

          <div class="panel">
            <div class="panel-title">城市排行</div>
            <ul class="rank-list">
              <li v-for="(item, index) in rankList" :key="item.city">
                <span class="rank-index">{{ index + 1 }}</span>
                <span class="rank-city">{{ item.city }}</span>
                <div class="rank-bar">
                  <i :style="{ width: item.percent + '%' }"></i>
                </div>
                <span class="rank-value">{{ item.value }}</span>
              </li>
            </ul>
          </div>
        </section>
      </main>
    </div>
  </ScaleScreen>
</template>

<script setup lang="ts">
import type { EChartsOption } from 'echarts'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import BaseChart from './components/BaseChart.vue'
import ScaleScreen from './components/ScaleScreen.vue'

const currentTime = ref('')
const isFullScreen = ref(false)

let timer: number | null = null

const updateTime = () => {
  const now = new Date()

  currentTime.value = now.toLocaleString('zh-CN', {
    hour12: false
  })
}

const toggleFullScreen = async () => {
  if (!document.fullscreenElement) {
    await document.documentElement.requestFullscreen()
    isFullScreen.value = true
  } else {
    await document.exitFullscreen()
    isFullScreen.value = false
  }
}

onMounted(() => {
  updateTime()

  timer = window.setInterval(() => {
    updateTime()
  }, 1000)

  document.addEventListener('fullscreenchange', () => {
    isFullScreen.value = Boolean(document.fullscreenElement)
  })
})

onUnmounted(() => {
  if (timer) {
    clearInterval(timer)
  }
})

const numberCards = [
  {
    label: '今日访问量',
    value: '128,560',
    unit: '次'
  },
  {
    label: '新增用户',
    value: '8,932',
    unit: '人'
  },
  {
    label: '订单总量',
    value: '36,218',
    unit: '单'
  },
  {
    label: '转化率',
    value: '28.6',
    unit: '%'
  }
]

const rankList = [
  {
    city: '广州',
    value: 12860,
    percent: 95
  },
  {
    city: '深圳',
    value: 11680,
    percent: 86
  },
  {
    city: '厦门',
    value: 9860,
    percent: 75
  },
  {
    city: '贵阳',
    value: 8920,
    percent: 66
  },
  {
    city: '成都',
    value: 7680,
    percent: 58
  }
]

const commonTextColor = '#b8d8ff'
const splitLineColor = 'rgba(255, 255, 255, 0.08)'

const barOption = computed<EChartsOption>(() => ({
  tooltip: {
    trigger: 'axis'
  },
  grid: {
    left: 45,
    right: 20,
    top: 40,
    bottom: 35
  },
  xAxis: {
    type: 'category',
    data: ['抖音', '小红书', '百度', '微信', '官网'],
    axisLabel: {
      color: commonTextColor,
      fontSize: 14
    },
    axisLine: {
      lineStyle: {
        color: 'rgba(255,255,255,0.18)'
      }
    }
  },
  yAxis: {
    type: 'value',
    axisLabel: {
      color: commonTextColor,
      fontSize: 14
    },
    splitLine: {
      lineStyle: {
        color: splitLineColor
      }
    }
  },
  series: [
    {
      name: '访问量',
      type: 'bar',
      barWidth: 24,
      data: [3200, 4300, 2600, 5200, 3900],
      itemStyle: {
        borderRadius: [8, 8, 0, 0],
        color: '#00d5ff'
      }
    }
  ]
}))

const lineOption = computed<EChartsOption>(() => ({
  tooltip: {
    trigger: 'axis'
  },
  grid: {
    left: 45,
    right: 25,
    top: 40,
    bottom: 35
  },
  xAxis: {
    type: 'category',
    data: ['1月', '2月', '3月', '4月', '5月', '6月'],
    axisLabel: {
      color: commonTextColor
    }
  },
  yAxis: {
    type: 'value',
    axisLabel: {
      color: commonTextColor
    },
    splitLine: {
      lineStyle: {
        color: splitLineColor
      }
    }
  },
  series: [
    {
      name: '增长量',
      type: 'line',
      smooth: true,
      symbolSize: 8,
      data: [120, 260, 300, 480, 620, 760],
      lineStyle: {
        width: 3,
        color: '#15f5ba'
      },
      itemStyle: {
        color: '#15f5ba'
      }
    }
  ]
}))

const pieOption = computed<EChartsOption>(() => ({
  tooltip: {
    trigger: 'item'
  },
  legend: {
    bottom: 0,
    textStyle: {
      color: commonTextColor
    }
  },
  series: [
    {
      name: '用户类型',
      type: 'pie',
      radius: ['45%', '68%'],
      center: ['50%', '45%'],
      avoidLabelOverlap: true,
      label: {
        color: '#fff'
      },
      data: [
        {
          name: '新用户',
          value: 42
        },
        {
          name: '老用户',
          value: 35
        },
        {
          name: '会员',
          value: 23
        }
      ]
    }
  ]
}))

const areaLineOption = computed<EChartsOption>(() => ({
  tooltip: {
    trigger: 'axis'
  },
  grid: {
    left: 55,
    right: 30,
    top: 40,
    bottom: 35
  },
  xAxis: {
    type: 'category',
    boundaryGap: false,
    data: ['08:00', '10:00', '12:00', '14:00', '16:00', '18:00', '20:00'],
    axisLabel: {
      color: commonTextColor
    }
  },
  yAxis: {
    type: 'value',
    axisLabel: {
      color: commonTextColor
    },
    splitLine: {
      lineStyle: {
        color: splitLineColor
      }
    }
  },
  series: [
    {
      name: '订单量',
      type: 'line',
      smooth: true,
      symbol: 'circle',
      symbolSize: 7,
      data: [120, 260, 420, 380, 660, 820, 760],
      lineStyle: {
        width: 3,
        color: '#00d5ff'
      },
      itemStyle: {
        color: '#00d5ff'
      },
      areaStyle: {
        color: 'rgba(0, 213, 255, 0.2)'
      }
    }
  ]
}))
</script>

<style scoped>
.big-screen {
  width: 1920px;
  height: 1080px;
  color: #fff;
  background:
    linear-gradient(rgba(4, 18, 45, 0.88), rgba(4, 18, 45, 0.88)),
    radial-gradient(circle at center, rgba(0, 213, 255, 0.2), transparent 50%);
}

.screen-header {
  position: relative;
  height: 92px;
  display: flex;
  align-items: center;
  justify-content: center;
  background:
    linear-gradient(90deg, transparent, rgba(0, 213, 255, 0.18), transparent);
  border-bottom: 1px solid rgba(0, 213, 255, 0.25);
}

.screen-header h1 {
  margin: 0;
  font-size: 38px;
  letter-spacing: 8px;
  text-shadow: 0 0 18px rgba(0, 213, 255, 0.8);
}

.header-left {
  position: absolute;
  left: 30px;
  top: 34px;
  font-size: 16px;
  color: #b8d8ff;
}

.fullscreen-btn {
  position: absolute;
  right: 30px;
  top: 26px;
  height: 40px;
  padding: 0 20px;
  color: #fff;
  cursor: pointer;
  background: rgba(0, 213, 255, 0.16);
  border: 1px solid rgba(0, 213, 255, 0.55);
  border-radius: 4px;
}

.screen-main {
  height: calc(100% - 92px);
  padding: 20px;
  display: grid;
  grid-template-columns: 450px 1fr 450px;
  gap: 20px;
}

.left-area,
.right-area {
  display: grid;
  grid-template-rows: 1fr 1fr;
  gap: 20px;
}

.center-area {
  display: grid;
  grid-template-rows: 150px 1fr 260px;
  gap: 20px;
}

.panel,
.map-panel,
.number-card {
  position: relative;
  overflow: hidden;
  background: rgba(6, 26, 62, 0.78);
  border: 1px solid rgba(0, 213, 255, 0.28);
  box-shadow:
    inset 0 0 24px rgba(0, 213, 255, 0.08),
    0 0 20px rgba(0, 213, 255, 0.05);
}

.panel::before,
.map-panel::before,
.number-card::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  width: 45%;
  height: 1px;
  background: linear-gradient(90deg, #00d5ff, transparent);
}

.panel-title,
.map-title {
  height: 48px;
  padding-left: 18px;
  display: flex;
  align-items: center;
  font-size: 20px;
  font-weight: bold;
  color: #ffffff;
}

.panel-title::before,
.map-title::before {
  content: '';
  width: 4px;
  height: 18px;
  margin-right: 10px;
  background: #00d5ff;
  box-shadow: 0 0 12px rgba(0, 213, 255, 0.9);
}

.chart-box {
  width: 100%;
  height: calc(100% - 48px);
}

.bottom-chart {
  height: 212px;
}

.number-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 20px;
}

.number-card {
  padding: 22px 24px;
}

.number-label {
  font-size: 17px;
  color: #b8d8ff;
}

.number-value {
  margin-top: 14px;
  font-size: 34px;
  font-weight: bold;
  color: #00d5ff;
  text-shadow: 0 0 16px rgba(0, 213, 255, 0.6);
}

.number-value span {
  margin-left: 6px;
  font-size: 16px;
  color: #ffffff;
}

.map-panel {
  padding: 0 20px 20px;
}

.map-content {
  position: relative;
  height: calc(100% - 48px);
  overflow: hidden;
  border: 1px solid rgba(0, 213, 255, 0.18);
  background:
    linear-gradient(90deg, rgba(0, 213, 255, 0.06) 1px, transparent 1px),
    linear-gradient(rgba(0, 213, 255, 0.06) 1px, transparent 1px);
  background-size: 48px 48px;
}

.map-center {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 160px;
  height: 160px;
  margin-left: -80px;
  margin-top: -80px;
  border-radius: 50%;
  background: rgba(0, 213, 255, 0.18);
  border: 1px solid rgba(0, 213, 255, 0.85);
  display: flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  font-size: 26px;
  box-shadow: 0 0 42px rgba(0, 213, 255, 0.45);
}

.map-circle {
  position: absolute;
  left: 50%;
  top: 50%;
  border-radius: 50%;
  border: 1px solid rgba(0, 213, 255, 0.35);
  transform: translate(-50%, -50%);
  animation: pulse 3s linear infinite;
}

.circle-1 {
  width: 280px;
  height: 280px;
}

.circle-2 {
  width: 440px;
  height: 440px;
  animation-delay: 0.5s;
}

.circle-3 {
  width: 620px;
  height: 620px;
  animation-delay: 1s;
}

.city {
  position: absolute;
  padding: 6px 14px;
  color: #fff;
  font-size: 18px;
  background: rgba(0, 213, 255, 0.2);
  border: 1px solid rgba(0, 213, 255, 0.55);
  border-radius: 16px;
}

.city-gz {
  left: 26%;
  top: 32%;
}

.city-sz {
  right: 22%;
  top: 36%;
}

.city-xm {
  left: 34%;
  bottom: 25%;
}

.city-gy {
  right: 32%;
  bottom: 30%;
}

.rank-list {
  height: calc(100% - 48px);
  margin: 0;
  padding: 20px;
  list-style: none;
}

.rank-list li {
  height: 50px;
  display: grid;
  grid-template-columns: 36px 70px 1fr 70px;
  align-items: center;
  gap: 12px;
  color: #d8efff;
}

.rank-index {
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: rgba(0, 213, 255, 0.25);
  text-align: center;
  line-height: 26px;
  color: #00d5ff;
}

.rank-city {
  font-size: 16px;
}

.rank-bar {
  height: 8px;
  overflow: hidden;
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.08);
}

.rank-bar i {
  display: block;
  height: 100%;
  border-radius: 10px;
  background: linear-gradient(90deg, #00d5ff, #15f5ba);
}

.rank-value {
  text-align: right;
  color: #ffffff;
}

@keyframes pulse {
  0% {
    opacity: 0.8;
    transform: translate(-50%, -50%) scale(0.92);
  }

  100% {
    opacity: 0.15;
    transform: translate(-50%, -50%) scale(1.08);
  }
}
</style>

九、ECharts 图表封装与使用

1. 图表容器必须有宽高

错误写法:

html 复制代码
<div class="chart"></div>
css 复制代码
.chart {
  width: 100%;
}

正确写法:

css 复制代码
.chart {
  width: 100%;
  height: 300px;
}

大屏项目推荐:

css 复制代码
.panel {
  height: 300px;
}

.chart-box {
  width: 100%;
  height: calc(100% - 48px);
}

.base-chart {
  width: 100%;
  height: 100%;
}

2. 图表销毁

ECharts 组件卸载时一定要销毁:

ts 复制代码
chartInstance?.dispose()

否则长时间运行可能出现内存泄漏。

3. 图表尺寸变化

当窗口变化、容器变化、全屏切换时,要调用:

ts 复制代码
chartInstance?.resize()

推荐使用:

ts 复制代码
const resizeObserver = new ResizeObserver(() => {
  chartInstance?.resize()
})

4. 柱状图示例

ts 复制代码
const barOption = {
  tooltip: {
    trigger: 'axis'
  },
  grid: {
    left: 40,
    right: 20,
    top: 40,
    bottom: 30
  },
  xAxis: {
    type: 'category',
    data: ['广州', '深圳', '厦门', '贵阳', '成都'],
    axisLabel: {
      color: '#b8d8ff'
    }
  },
  yAxis: {
    type: 'value',
    axisLabel: {
      color: '#b8d8ff'
    },
    splitLine: {
      lineStyle: {
        color: 'rgba(255,255,255,0.1)'
      }
    }
  },
  series: [
    {
      name: '访问量',
      type: 'bar',
      data: [1200, 1800, 900, 1500, 2100],
      barWidth: 18,
      itemStyle: {
        borderRadius: [8, 8, 0, 0],
        color: '#00cfff'
      }
    }
  ]
}

5. dataset 数据管理

ECharts 可以使用 dataset 管理数据,让数据和图表配置分离。

ts 复制代码
const option = {
  dataset: {
    source: [
      ['product', '2024', '2025', '2026'],
      ['Vue', 43, 85, 120],
      ['React', 83, 73, 95],
      ['Node', 86, 65, 110]
    ]
  },
  tooltip: {},
  legend: {
    textStyle: {
      color: '#fff'
    }
  },
  xAxis: {
    type: 'category'
  },
  yAxis: {},
  series: [
    { type: 'bar' },
    { type: 'bar' },
    { type: 'bar' }
  ]
}

适合:

txt 复制代码
1. 多个图表复用同一份数据
2. 后端返回二维表数据
3. 图表配置和数据逻辑分离
4. 降低 series.data 重复维护

十、地图大屏开发

地图是很多大屏的核心区域。

1. 常见地图方案

方案 适合场景
ECharts 地图 中国地图、省市地图、热力图、飞线图
高德地图 JS API 真实地图、路线规划、点位标注
百度地图 API 国内地图业务系统
Mapbox / MapLibre 高级地图可视化、自定义图层
Three.js 3D 地球、3D 城市、立体地图
Cesium GIS、三维地球、智慧城市

2. ECharts 地图适合做什么

txt 复制代码
1. 中国地图
2. 省份地图
3. 城市数据分布
4. 区域热力图
5. 飞线图
6. 散点图
7. 数据下钻
8. 地理位置标记

3. ECharts 地图示例

ts 复制代码
const mapOption = {
  tooltip: {
    trigger: 'item'
  },
  visualMap: {
    min: 0,
    max: 1000,
    left: 20,
    bottom: 20,
    text: ['高', '低'],
    textStyle: {
      color: '#fff'
    },
    inRange: {
      color: ['#163f7a', '#00cfff', '#f6d365']
    }
  },
  series: [
    {
      name: '城市数据',
      type: 'map',
      map: 'china',
      roam: true,
      label: {
        show: true,
        color: '#fff'
      },
      data: [
        {
          name: '广东',
          value: 900
        },
        {
          name: '贵州',
          value: 500
        },
        {
          name: '四川',
          value: 700
        }
      ]
    }
  ]
}

4. 地图开发注意事项

txt 复制代码
1. GeoJSON 数据不要太大
2. 地图名称要和数据中的 name 对得上
3. 飞线动画不要太多
4. 地图最好单独封装成组件
5. 地图数据要提前缓存
6. 地图容器变化后也要 resize

十一、接口数据接入

大屏数据一般来自后端接口。

1. 安装 Axios

bash 复制代码
npm install axios

2. 封装请求

src/api/request.ts

ts 复制代码
import axios from 'axios'

const service = axios.create({
  baseURL: '/api',
  timeout: 10000
})

service.interceptors.request.use(
  config => {
    return config
  },
  error => {
    return Promise.reject(error)
  }
)

service.interceptors.response.use(
  response => {
    return response.data
  },
  error => {
    console.error('接口请求错误:', error)
    return Promise.reject(error)
  }
)

export default service

3. 大屏接口

src/api/screen.ts

ts 复制代码
import request from './request'

export function getScreenOverview() {
  return request.get('/screen/overview')
}

export function getChartData() {
  return request.get('/screen/chart')
}

export function getRankData() {
  return request.get('/screen/rank')
}

4. 页面中使用接口

ts 复制代码
import { onMounted, ref } from 'vue'
import { getScreenOverview } from '@/api/screen'

const overview = ref(null)

const loadOverview = async () => {
  try {
    const res = await getScreenOverview()
    overview.value = res
  } catch (error) {
    console.error('获取大屏数据失败', error)
  }
}

onMounted(() => {
  loadOverview()
})

5. 定时轮询

适合不是特别实时的数据,比如每 10 秒、30 秒刷新一次。

ts 复制代码
import { onMounted, onUnmounted } from 'vue'
import { getScreenOverview } from '@/api/screen'

let timer: number | null = null

const loadData = async () => {
  const res = await getScreenOverview()
  console.log(res)
}

onMounted(() => {
  loadData()

  timer = window.setInterval(() => {
    loadData()
  }, 10000)
})

onUnmounted(() => {
  if (timer) {
    clearInterval(timer)
  }
})

6. 轮询封装

src/composables/usePolling.ts

ts 复制代码
import { onMounted, onUnmounted } from 'vue'

interface UsePollingOptions {
  immediate?: boolean
  interval?: number
}

export function usePolling(
  callback: () => void | Promise<void>,
  options: UsePollingOptions = {}
) {
  const immediate = options.immediate ?? true
  const interval = options.interval ?? 10000

  let timer: number | null = null

  const start = () => {
    stop()

    if (immediate) {
      callback()
    }

    timer = window.setInterval(() => {
      callback()
    }, interval)
  }

  const stop = () => {
    if (timer) {
      clearInterval(timer)
      timer = null
    }
  }

  onMounted(() => {
    start()
  })

  onUnmounted(() => {
    stop()
  })

  return {
    start,
    stop
  }
}

使用:

ts 复制代码
usePolling(async () => {
  const res = await getScreenOverview()
  console.log(res)
}, {
  interval: 10000
})

十二、WebSocket 实时数据推送

WebSocket 适合实时性强的大屏:

txt 复制代码
1. 实时告警
2. 实时订单
3. 实时设备状态
4. 实时在线人数
5. 实时物流轨迹
6. 实时生产数据

1. WebSocket 基础写法

ts 复制代码
const ws = new WebSocket('ws://localhost:3000/screen')

ws.onopen = () => {
  console.log('WebSocket 已连接')
}

ws.onmessage = event => {
  const data = JSON.parse(event.data)
  console.log(data)
}

ws.onerror = error => {
  console.error('WebSocket 错误', error)
}

ws.onclose = () => {
  console.log('WebSocket 已关闭')
}

2. WebSocket 封装

src/composables/useWebSocket.ts

ts 复制代码
import { onMounted, onUnmounted, ref } from 'vue'

interface UseWebSocketOptions {
  reconnect?: boolean
  reconnectInterval?: number
}

export function useWebSocket(url: string, options: UseWebSocketOptions = {}) {
  const data = ref<any>(null)
  const status = ref<'connecting' | 'open' | 'closed' | 'error'>('closed')

  const reconnect = options.reconnect ?? true
  const reconnectInterval = options.reconnectInterval ?? 3000

  let ws: WebSocket | null = null
  let reconnectTimer: number | null = null

  const connect = () => {
    status.value = 'connecting'
    ws = new WebSocket(url)

    ws.onopen = () => {
      status.value = 'open'
      console.log('WebSocket 已连接')
    }

    ws.onmessage = event => {
      try {
        data.value = JSON.parse(event.data)
      } catch {
        data.value = event.data
      }
    }

    ws.onerror = () => {
      status.value = 'error'
    }

    ws.onclose = () => {
      status.value = 'closed'

      if (reconnect) {
        reconnectTimer = window.setTimeout(() => {
          connect()
        }, reconnectInterval)
      }
    }
  }

  const send = (message: unknown) => {
    if (ws && ws.readyState === WebSocket.OPEN) {
      ws.send(typeof message === 'string' ? message : JSON.stringify(message))
    }
  }

  const close = () => {
    if (reconnectTimer) {
      clearTimeout(reconnectTimer)
      reconnectTimer = null
    }

    ws?.close()
    ws = null
  }

  onMounted(() => {
    connect()
  })

  onUnmounted(() => {
    close()
  })

  return {
    data,
    status,
    send,
    close,
    connect
  }
}

使用:

ts 复制代码
const { data, status } = useWebSocket('ws://localhost:3000/screen', {
  reconnect: true,
  reconnectInterval: 3000
})

十三、大屏视觉设计要点

1. 背景设计

常用深色科技风背景:

css 复制代码
.screen-bg {
  background:
    radial-gradient(circle at center, rgba(0, 213, 255, 0.2), transparent 50%),
    linear-gradient(135deg, #020817 0%, #061932 55%, #020817 100%);
}

2. 面板设计

css 复制代码
.panel {
  background: rgba(6, 21, 43, 0.85);
  border: 1px solid rgba(0, 255, 255, 0.25);
  box-shadow: inset 0 0 20px rgba(0, 255, 255, 0.12);
}

3. 标题设计

css 复制代码
.panel-title {
  font-size: 20px;
  color: #ffffff;
  font-weight: bold;
  padding-left: 16px;
  position: relative;
}

.panel-title::before {
  content: '';
  position: absolute;
  left: 0;
  top: 6px;
  width: 4px;
  height: 18px;
  background: #00eaff;
}

4. 指标卡设计

vue 复制代码
<template>
  <div class="number-card">
    <div class="label">{{ label }}</div>
    <div class="value">{{ value }}</div>
    <div class="unit">{{ unit }}</div>
  </div>
</template>

<script setup lang="ts">
defineProps<{
  label: string
  value: number | string
  unit?: string
}>()
</script>

<style scoped>
.number-card {
  padding: 20px;
  background: rgba(0, 198, 255, 0.08);
  border: 1px solid rgba(0, 198, 255, 0.25);
}

.label {
  font-size: 16px;
  color: #9bc8ff;
}

.value {
  margin-top: 10px;
  font-size: 36px;
  font-weight: bold;
  color: #00eaff;
}

.unit {
  font-size: 14px;
  color: #fff;
}
</style>

5. 动效建议

适合使用的动效:

txt 复制代码
1. 数字滚动
2. 呼吸灯
3. 扫描线
4. 地图涟漪点
5. 飞线动画
6. 边框光效
7. 排行榜滚动
8. 折线图动态更新

不建议过度使用:

txt 复制代码
1. 大量粒子动画
2. 大面积模糊滤镜
3. 过多 box-shadow
4. 所有元素同时闪烁
5. 高频 DOM 动画

十四、全屏展示处理

1. 进入全屏

ts 复制代码
await document.documentElement.requestFullscreen()

2. 退出全屏

ts 复制代码
await document.exitFullscreen()

3. 判断是否全屏

ts 复制代码
const isFullScreen = Boolean(document.fullscreenElement)

4. 监听全屏变化

ts 复制代码
document.addEventListener('fullscreenchange', () => {
  isFullScreen.value = Boolean(document.fullscreenElement)
})

5. 注意事项

txt 复制代码
1. requestFullscreen 通常必须由用户点击触发
2. 进入全屏后窗口尺寸会变化,需要重新计算 scale
3. ECharts 图表可能需要 resize
4. 浏览器可能会显示全屏提示

十五、性能优化重点

大屏项目最容易卡在:

txt 复制代码
图表太多
动画太多
数据刷新太频繁
地图数据太大
DOM 节点太多
WebSocket 数据推送太密集
长时间运行内存泄漏

1. 图表优化

txt 复制代码
1. 图表组件卸载时必须 dispose
2. 图表数据不要无限增长
3. 大量数据先聚合再展示
4. 隐藏状态下不要频繁 resize
5. 切换 tab 后手动 resize
6. 地图 GeoJSON 要压缩

2. 接口优化

txt 复制代码
1. 普通数据 10~30 秒轮询即可
2. 实时数据用 WebSocket
3. 不要多个组件重复请求同一个接口
4. 请求失败要有兜底
5. 页面卸载要清除定时器

3. CSS 动画优化

txt 复制代码
1. 优先动画 transform 和 opacity
2. 少用 width、height、top、left 做动画
3. 少用大量 filter、blur、box-shadow
4. 动画元素数量不要过多
5. 长时间运行要测试性能

4. 图片资源优化

txt 复制代码
1. 背景图尽量压缩
2. 推荐使用 webp
3. 小图标可以用 svg
4. 不要使用过大的透明 png
5. 静态资源走 CDN 更好

5. 内存泄漏检查点

txt 复制代码
1. setInterval 是否 clearInterval
2. WebSocket 是否 close
3. ECharts 是否 dispose
4. ResizeObserver 是否 disconnect
5. window 事件是否 removeEventListener
6. 地图实例是否销毁

十六、部署上线

1. 打包项目

bash 复制代码
npm run build

打包后会生成:

txt 复制代码
dist

2. Nginx 部署

dist 目录上传到服务器,例如:

txt 复制代码
/www/wwwroot/big-screen

Nginx 配置:

nginx 复制代码
server {
    listen 80;
    server_name your-domain.com;

    root /www/wwwroot/big-screen;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }

    location /api/ {
        proxy_pass http://127.0.0.1:3000/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    gzip on;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
}

3. WebSocket 代理

如果有 WebSocket,需要配置:

nginx 复制代码
location /ws/ {
    proxy_pass http://127.0.0.1:3000/;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "Upgrade";
    proxy_set_header Host $host;
}

4. 服务器测试重点

txt 复制代码
1. 真实大屏分辨率是否正常
2. 是否出现滚动条
3. 是否有左右或上下留白
4. 图表是否完整显示
5. 全屏是否正常
6. 接口是否跨域
7. WebSocket 是否能连接
8. 长时间运行是否卡顿

十七、常见问题与解决方案

1. 大屏左右有黑边是不是 bug?

不是。

如果屏幕比例和设计稿比例不一致,为了保证内容不变形,就会有留白。

解决方式:

txt 复制代码
1. 背景铺满整个屏幕
2. 内容保持 16:9 居中显示
3. 允许两侧或上下留白

2. 想强制铺满整个屏幕怎么办?

可以宽高分别缩放:

ts 复制代码
const scaleX = window.innerWidth / designWidth
const scaleY = window.innerHeight / designHeight

然后:

css 复制代码
transform: scale(scaleX, scaleY);

但是不推荐,因为会导致:

txt 复制代码
圆形变椭圆
地图变形
字体比例失真
图表视觉失真

3. 图表初始化不显示怎么办?

检查:

txt 复制代码
1. 图表 DOM 是否有宽高
2. 是否在 mounted 之后初始化
3. 父容器高度是不是 0
4. 是否在 display: none 状态下初始化
5. 显示后有没有调用 chart.resize()

4. 页面缩放后文字模糊怎么办?

原因:

txt 复制代码
1. scale 不是整数
2. 浏览器抗锯齿导致
3. 字体太小

优化:

txt 复制代码
1. 字体不要太小
2. 标题和数字适当加粗
3. 重要数字建议 24px 以上
4. 设置最大缩放 maxScale
5. 使用清晰字体

5. 为什么不用全部写百分比?

百分比布局虽然能随屏幕变化,但很难还原设计稿。

大屏更需要稳定的视觉比例,所以推荐:

txt 复制代码
外层 scale 负责整体适配
内部按设计稿使用 px 开发
局部使用 grid / flex 布局

6. 4K 屏怎么适配?

如果设计稿是 1920×1080,4K 是 3840×2160

txt 复制代码
scale = 2

页面会等比放大 2 倍。

如果不想放太大,可以限制:

vue 复制代码
<ScaleScreen :width="1920" :height="1080" :max-scale="1.5">
  ...
</ScaleScreen>

7. 移动端能不能看大屏?

可以预览,但不建议作为主体验。

如果确实要移动端访问,建议单独做移动端页面,而不是把 1920×1080 大屏直接缩到手机上。


十八、学习资源与官方文档

类型 资源 链接
Vue3 官方文档 Vue Guide https://vuejs.org/guide/introduction
Vue SFC 单文件组件规范 https://vuejs.org/api/sfc-spec
Vite 官方文档 Vite Guide https://vite.dev/guide/
Vite 中文文档 Vite 中文指南 https://cn.vite.dev/guide/
ECharts 官网 Apache ECharts https://echarts.apache.org/
ECharts 快速开始 Get Started https://echarts.apache.org/handbook/en/get-started/
ECharts 图表尺寸 Chart Container and Size https://apache.github.io/echarts-handbook/en/concepts/chart-size/
ECharts Dataset Dataset https://apache.github.io/echarts-handbook/en/concepts/dataset/
ECharts visualMap Visual Map https://apache.github.io/echarts-handbook/en/concepts/visual-map/
ECharts API API 文档 https://echarts.apache.org/en/api.html
ResizeObserver MDN ResizeObserver https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver
CSS transform scale MDN scale() https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/transform-function/scale
Fullscreen API MDN Fullscreen API https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API
CSS Grid MDN CSS Grid https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_grid_layout
CSS Flexbox MDN Flexbox https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_flexible_box_layout
Axios Axios 文档 https://axios-http.com/docs/intro
Pinia Pinia 官方文档 https://pinia.vuejs.org/
D3.js D3 官方网站 https://d3js.org/
Three.js Three.js 官方文档 https://threejs.org/docs/
Mapbox GL JS Mapbox 文档 https://docs.mapbox.com/mapbox-gl-js/guides/
MapLibre GL JS MapLibre 文档 https://maplibre.org/maplibre-gl-js/docs/
Nginx Nginx 官方文档 https://nginx.org/en/docs/

十九、大屏开发总结

数据大屏开发最核心的能力是:

txt 复制代码
1. 会布局:CSS Grid、Flex、固定设计稿布局
2. 会适配:1920×1080 + transform scale + 居中补边
3. 会图表:ECharts option、series、dataset、tooltip、legend、visualMap
4. 会封装:BaseChart、ScaleScreen、ScreenPanel、NumberCard
5. 会接口:Axios、接口封装、错误处理、轮询刷新
6. 会实时数据:WebSocket、重连、关闭连接
7. 会地图:ECharts map、geo、飞线、散点、热力图
8. 会动效:CSS 动画、数字滚动、呼吸灯、扫描线
9. 会优化:resize、dispose、防抖、数据聚合、内存清理
10. 会部署:Vite 打包、Nginx、接口代理、WebSocket 代理

最终推荐方案:

txt 复制代码
Vue3 + TypeScript + Vite
+
ECharts
+
1920×1080 固定设计稿
+
transform scale 等比缩放
+
ResizeObserver 图表自适应
+
Axios / WebSocket 数据接入
+
Nginx 部署上线

如果是刚开始学习大屏开发,建议按下面顺序练习:

txt 复制代码
第一阶段:完成 1920×1080 静态大屏页面
第二阶段:封装 ScaleScreen 自适应组件
第三阶段:封装 BaseChart 图表组件
第四阶段:接入柱状图、折线图、饼图
第五阶段:实现指标卡、排行榜、模拟地图
第六阶段:接入真实接口数据
第七阶段:实现轮询和 WebSocket
第八阶段:优化动画、性能和部署

掌握这套流程后,就可以独立完成大多数企业数据大屏、运营驾驶舱、智慧展示屏项目。

相关推荐
陈_杨1 小时前
鸿蒙APP开发-带你了解单块酷APP参数管理的功能
前端·javascript
moMo1 小时前
# 从重置样式到 BEM 命名:写一个微信的按钮
前端·css
2301_815645381 小时前
saas 一面
前端·面经
无风听海1 小时前
OAuth 2.0 Scope 的使用与设计规划
前端
2501_916008891 小时前
全面解析常用Web前端开发工具:编辑器、调试工具、性能分析器与框架
android·前端·ios·小程序·uni-app·编辑器·iphone
暗夜猎手-大魔王1 小时前
转载--Hermes Agent 08 | Agent 的自我进化:nudge、后台审查与轨迹数据
java·前端·人工智能
IT_陈寒1 小时前
Redis集群节点迁移把我坑惨了,这个坑你得提前绕开
前端·人工智能·后端
新酱爱学习2 小时前
手搓 10 个 Skill 踩出来的坑,我做成了一套工程化工具链
前端·人工智能·agent
怕浪猫2 小时前
Electron 开发实战(八):多媒体处理全解|音视频播放、录屏、FFmpeg 实战
前端·javascript·electron