1

大屏开发概述

📊 什么是数据大屏 (Dashboard)?

数据大屏(数据可视化大屏)是将企业关键业务指标以图表、数字、地图等形式集中展示在大屏幕上的系统。 在制造业场景中,大屏通常部署在车间墙壁上的大尺寸电视或 LED 屏上,实时监控产线状态。

类比理解

普通网站 / 手机APP 就像你手机上的天气APP — 你主动打开,滚动查看,随时操作。
数据大屏 就像工厂车间墙上挂的那块大屏幕 — 永远亮着,不用你操作,抬头就能看到 OEE、产量、告警等关键信息。

再想一个:普通网页 = 你在工位上用 Excel 看报表;数据大屏 = 老板会议室墙上那块实时跳动的仪表盘。
大屏的核心理念:一眼看到全局,数据自己说话。

数据大屏 vs 普通网页

普通网页

  • 响应式布局,适配各种屏幕
  • 支持滚动,内容不限
  • 浅色/暗色主题随意
  • 用户交互为主(点击、表单)
  • 字体 14-16px
  • 数据按需加载
  • 使用 React/Vue 等框架

数据大屏

  • 固定分辨率(1920×1080 为主)
  • 一屏展示,禁止滚动
  • 暗色主题(科技感/深色系)
  • 数据展示为主(少交互)
  • 大字体(标题 20-36px)
  • 数据实时轮询 / WebSocket
  • 可不用框架,纯 HTML + ECharts
特性普通网页数据大屏
分辨率响应式固定 1920×1080(scale 适配)
滚动支持禁止,所有内容一屏展示
主题浅色为主暗色主题(深蓝/黑色背景)
交互点击、表单、路由跳转极少交互,主要是数据展示
刷新用户手动刷新自动轮询(5-30秒)或 WebSocket
字体14-16px标题 24-48px,数据 20-36px
运行环境浏览器大屏 TV / LED 显示器
技术栈Vue/React + 组件库ECharts + 原生 CSS/Canvas
2

页面布局方案

📐 经典大屏布局结构

制造业大屏的布局几乎都是「上-中-下」三段式,中间分「左-中-右」三列。

Header — 厂名 + 实时时钟 + 日期
OEE: 85.2%
直通率: 96.8%
计划达成率: 92.1%
Center — 产线产量趋势 / 车间地图
停机时长 TOP5
能耗趋势
Footer — 滚动告警信息栏
区域内容宽度占比
Header 标题栏工厂名称、实时时钟、日期100%
Left 左侧面板2-3 个指标卡(OEE、直通率、计划达成率)~25%
Center 中央区域主图表:产线产量趋势 / 车间地图 / 综合仪表~50%
Right 右侧面板停机时长 TOP5、能耗趋势~25%
Footer 底部滚动告警信息(设备故障、质量异常等)100%
💻 完整布局代码
dashboard-layout.htmlHTML + CSS
/* 大屏核心布局:1920×1080 固定尺寸 + scale 缩放 */
<!DOCTYPE html>
<html>
<head>
  <style>
    /* 设计稿基准尺寸 */
    body {
      margin: 0;
      background: #0a0e27;
      color: #e0e6ff;
      font-family: 'Microsoft YaHei', sans-serif;
      overflow: hidden;  /* 禁止滚动 */
    }

    /* 缩放容器:以 1920×1080 为基准 */
    .screen-wrapper {
      width: 1920px;
      height: 1080px;
      transform-origin: left top;
      position: absolute;
      top: 50%; left: 50%;
    }

    /* Header:标题栏 */
    .header {
      height: 70px;
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: 0 30px;
      background: linear-gradient(180deg, rgba(0,242,254,0.1), transparent);
      border-bottom: 1px solid #7ec8f0;
    }
    .header .title {
      font-size: 28px;
      font-weight: bold;
      letter-spacing: 6px;
      background: linear-gradient(90deg, #00f2fe, #4facfe);
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
    }

    /* 主体三列布局 */
    .main-body {
      display: flex;
      height: calc(1080px - 70px - 44px);
      padding: 12px;
      gap: 12px;
    }
    .panel-left, .panel-right {
      width: 460px;
      display: flex;
      flex-direction: column;
      gap: 12px;
    }
    .panel-center {
      flex: 1;
      display: flex;
      flex-direction: column;
      gap: 12px;
    }

    /* 卡片通用样式 */
    .chart-card {
      background: rgba(13,21,58,0.8);
      border: 1px solid #7ec8f0;
      border-radius: 8px;
      padding: 16px;
      flex: 1;
    }

    /* Footer:底部告警滚动条 */
    .footer {
      height: 44px;
      background: rgba(0,0,0,0.3);
      border-top: 1px solid #7ec8f0;
      display: flex;
      align-items: center;
      padding: 0 20px;
      font-size: 14px;
      color: #ffd93d;
      overflow: hidden;
    }
  </style>
</head>
<body>
  <div class="screen-wrapper">
    <!-- Header -->
    <div class="header">
      <span class="title">XX工厂 智能制造数据中心</span>
      <span id="clock">2026-06-10 14:30:00</span>
    </div>

    <!-- 主体 -->
    <div class="main-body">
      <div class="panel-left">
        <div class="chart-card"> OEE 指标卡 </div>
        <div class="chart-card"> 直通率 </div>
        <div class="chart-card"> 计划达成率 </div>
      </div>
      <div class="panel-center">
        <div class="chart-card"> 产线产量趋势 </div>
      </div>
      <div class="panel-right">
        <div class="chart-card"> 停机时长TOP5 </div>
        <div class="chart-card"> 能耗趋势 </div>
      </div>
    </div>

    <!-- Footer -->
    <div class="footer">
      <span>⚠ [14:25] 3号线焊装工位温度异常 </span>
    </div>
  </div>
</body>
</html>
3

关键技术点

🖥️ 屏幕适配 — 三种方案对比

大屏永远以 1920×1080 为设计基准,但实际屏幕可能尺寸不同。三种适配方案:

方案原理优点缺点推荐度
scale 缩放 固定 1920×1080 设计,用 CSS transform: scale() 等比缩放 最简单,像素级还原设计稿 极端比例下会留白 推荐
rem + font-size 根据屏幕宽度动态设置 html font-size,所有尺寸用 rem 灵活,不留白 需要换算,字体可能模糊 可选
vw/vh 所有尺寸用 vw / vh 单位 原生支持,不需要 JS 非等比屏幕会变形 备选
推荐方案:大屏项目用 scale 缩放方案。因为大屏 TV 的分辨率固定(几乎都是 16:9),scale 方案最简单、最稳定。
方案一:Scale 缩放(推荐)JavaScript
// scale 适配:以 1920×1080 为基准,等比缩放
function autoScale() {
  const designWidth = 1920;
  const designHeight = 1080;
  const wrapper = document.querySelector('.screen-wrapper');

  const scaleX = window.innerWidth / designWidth;
  const scaleY = window.innerHeight / designHeight;
  const scale = Math.min(scaleX, scaleY);

  wrapper.style.transform = `scale(${scale})`;
  wrapper.style.transformOrigin = 'left top';
  // 居中
  wrapper.style.left = (window.innerWidth - designWidth * scale) / 2 + 'px';
  wrapper.style.top = (window.innerHeight - designHeight * scale) / 2 + 'px';
}
window.addEventListener('resize', autoScale);
autoScale();
方案二:rem 动态 font-sizeJavaScript
// rem 方案:根据屏幕宽度设置 html 的 font-size
function setRem() {
  const baseWidth = 1920;
  const baseFontSize = 100; // 1rem = 100px @1920
  const scale = document.documentElement.clientWidth / baseWidth;
  document.documentElement.style.fontSize = baseFontSize * scale + 'px';
}
// CSS 中:width: 4.6rem (=460px @1920), height: 1.08rem
方案三:vw/vh 单位CSS
/* vw/vh 方案:所有尺寸用视口单位 */
/* 1920px = 100vw, 1080px = 100vh */
/* 所以 460px = 23.96vw */
.panel-left {
  width: 23.96vw;  /* 460 / 1920 * 100 */
}
.header {
  height: 6.48vh;   /* 70 / 1080 * 100 */
  font-size: 1.46vw; /* 28 / 1920 * 100 */
}
🔄 实时数据更新 — 轮询 vs WebSocket

setInterval 轮询

// 每10秒请求一次
const timer = setInterval(async () => {
  const res = await fetch('/api/oee');
  const data = await res.json();
  updateCharts(data);
}, 10000);

// 页面销毁时清除
clearInterval(timer);
  • 实现简单,不需要后端额外支持
  • 适合数据变化不频繁的场景(5-30秒更新)
  • 浪费带宽(即使数据没变也会请求)

WebSocket 推送

// 建立 WebSocket 连接
const ws = new WebSocket('ws://server/oee');
ws.onmessage = (e) => {
  const data = JSON.parse(e.data);
  updateCharts(data);
};
ws.onerror = () => {
  // 断线重连
  setTimeout(connect, 3000);
};
  • 实时性高,服务端有变化就推送
  • 适合秒级更新、告警推送
  • 需要后端 WebSocket 支持
选型建议:制造业大屏一般用 setInterval 轮询(10-30秒)就够了。如果告警需要秒级推送,再加 WebSocket。
🔢 数字滚动动画 (CountUp Effect)

大屏上数字从旧值滚动到新值,是制造科技感的重要细节。

countUp.js — 数字滚动动画JavaScript
/**
 * 数字滚动动画
 * @param {HTMLElement} el - 目标 DOM 元素
 * @param {number} start - 起始值
 * @param {number} end - 目标值
 * @param {number} duration - 动画时长(ms)
 * @param {number} decimals - 小数位数
 */
function animateNumber(el, start, end, duration = 1000, decimals = 1) {
  const range = end - start;
  const startTime = performance.now();

  function step(currentTime) {
    const elapsed = currentTime - startTime;
    const progress = Math.min(elapsed / duration, 1);

    // easeOutExpo 缓动函数
    const eased = progress === 1 ? 1 : 1 - Math.pow(2, -10 * progress);
    const current = start + range * eased;

    el.textContent = current.toFixed(decimals) + '%';

    if (progress < 1) {
      requestAnimationFrame(step);
    }
  }
  requestAnimationFrame(step);
}

// 使用示例
const oeeEl = document.getElementById('oee-value');
animateNumber(oeeEl, 82.5, 85.2, 1500, 1);
// 数字会从 82.5% 平滑滚动到 85.2%
科技感边框装饰

大屏卡片的边框不是简单的 border,而是带有发光、渐变、角标的科技感边框。

Sci-Fi Border 样式CSS
/* 科技感边框:渐变边框 + 角标装饰 */
.sci-fi-border {
  position: relative;
  background: linear-gradient(135deg, rgba(13,21,58,0.9), rgba(6,10,31,0.95));
  border-radius: 4px;
  padding: 16px;
}

/* 外发光边框 */
.sci-fi-border::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 4px;
  padding: 1px;
  background: linear-gradient(135deg, #00f2fe33, #4facfe22, #d2a8ff33);
  -webkit-mask: linear-gradient(#fff 0 0) content-box,
                  linear-gradient(#fff 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
}

/* 四角装饰线 */
.sci-fi-border .corner {
  position: absolute;
  width: 12px; height: 12px;
  border-color: #00f2fe;
  border-style: solid;
}
.corner.tl { top:0;left:0;border-width:2px 0 0 2px; }
.corner.tr { top:0;right:0;border-width:2px 2px 0 0; }
.corner.bl { bottom:0;left:0;border-width:0 0 2px 2px; }
.corner.br { bottom:0;right:0;border-width:0 2px 2px 0; }
🎨 暗色主题配色方案

大屏的暗色主题不是随便选个深色背景就行,需要一套完整的色彩体系。

背景主色
#0a0e27
主要强调色
#00f2fe
次要色
#4facfe
警告色
#ffd93d
用途颜色说明
背景#0a0e27深蓝黑色,不刺眼
卡片背景rgba(13,21,58,0.8)半透明深蓝,有层次感
主文字#e0e6ff柔白色,高对比但不刺眼
强调文字#00f2fe青色,科技感强
数据色1#4facfe蓝色系图表
数据色2#22c55e绿色系(达标/正常)
数据色3#ffd93d黄色系(警告/注意)
数据色4#ff6b6b红色系(告警/异常)
边框#7ec8f0深蓝灰,若隐若现
注意:暗色主题中 千万不要用纯白色文字(#fff),用 #e0e6ff 这种带一点蓝调的柔白色,长时间盯屏不会刺眼。
4

完整示例:OEE 智能制造看板

👁️ 运行效果预览

打开下面的完整 HTML 文件,你会看到一个包含以下内容的制造业数据大屏:

XX工厂 智能制造 OEE 看板    2026-06-10 14:30:25
OEE
85.2%
直通率
96.8%
计划达成率
92.1%
停机时长
2.5h
各产线产量 (柱状图)
综合OEE (仪表盘)
24h OEE趋势 (折线图)
停机原因分布 (饼图)
📝 完整源码 — 可直接复制运行
使用方式:将以下代码全部复制保存为 oee-dashboard.html,直接用浏览器打开即可看到完整大屏效果。依赖 ECharts CDN,需要联网。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>OEE 智能制造看板</title>
  <script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body {
      background: #050a1a;
      color: #e0e6ff;
      font-family: 'Microsoft YaHei', sans-serif;
      overflow: hidden;
    }

    /* === Scale 适配容器 === */
    .screen {
      width: 1920px; height: 1080px;
      position: absolute; top: 0; left: 0;
      transform-origin: left top;
      display: flex; flex-direction: column;
    }

    /* === Header === */
    .header {
      height: 70px; display: flex;
      align-items: center; justify-content: center;
      position: relative;
      background: linear-gradient(180deg,
        rgba(0,242,254,0.08) 0%, transparent 100%);
      border-bottom: 1px solid #1a2550;
    }
    .header h1 {
      font-size: 30px; letter-spacing: 8px; font-weight: 700;
      background: linear-gradient(90deg, #00f2fe, #4facfe, #d2a8ff);
      -webkit-background-clip: text;
      -webkit-text-fill-color: transparent;
    }
    .header .clock {
      position: absolute; right: 30px;
      font-size: 18px; color: #4facfe;
      font-family: 'Consolas', monospace;
    }
    .header .date-info {
      position: absolute; left: 30px;
      font-size: 14px; color: #bac2de;
    }

    /* === KPI Cards === */
    .kpi-row {
      display: grid; grid-template-columns: repeat(4, 1fr);
      gap: 12px; padding: 14px 16px 0;
    }
    .kpi-card {
      background: linear-gradient(135deg, rgba(13,21,58,0.9), rgba(6,10,31,0.95));
      border: 1px solid #1a2550; border-radius: 8px;
      padding: 16px 20px; position: relative; overflow: hidden;
    }
    .kpi-card::before {
      content: ''; position: absolute; top: 0; left: 0; right: 0;
      height: 2px;
    }
    .kpi-card:nth-child(1)::before { background: linear-gradient(90deg, #00f2fe, transparent); }
    .kpi-card:nth-child(2)::before { background: linear-gradient(90deg, #22c55e, transparent); }
    .kpi-card:nth-child(3)::before { background: linear-gradient(90deg, #4facfe, transparent); }
    .kpi-card:nth-child(4)::before { background: linear-gradient(90deg, #ffd93d, transparent); }
    .kpi-label { font-size: 13px; color: #bac2de; margin-bottom: 6px; }
    .kpi-value { font-size: 32px; font-weight: 700; font-family: 'Consolas', monospace; }
    .kpi-value.cyan { color: #00f2fe; }
    .kpi-value.green { color: #22c55e; }
    .kpi-value.blue { color: #4facfe; }
    .kpi-value.yellow { color: #ffd93d; }
    .kpi-trend { font-size: 12px; margin-top: 4px; }
    .kpi-trend.up { color: #22c55e; }
    .kpi-trend.down { color: #ff6b6b; }

    /* === Chart Grid === */
    .chart-grid {
      flex: 1; display: grid;
      grid-template-columns: 2fr 1fr;
      grid-template-rows: 1fr 1fr;
      gap: 12px; padding: 12px 16px;
    }
    .chart-box {
      background: linear-gradient(135deg, rgba(13,21,58,0.9), rgba(6,10,31,0.95));
      border: 1px solid #1a2550; border-radius: 8px;
      padding: 12px; display: flex; flex-direction: column;
    }
    .chart-title {
      font-size: 14px; color: #bac2de;
      margin-bottom: 8px; padding-left: 8px;
      border-left: 3px solid #4facfe;
    }
    .chart-container { flex: 1; }

    /* === Footer === */
    .footer {
      height: 40px; display: flex; align-items: center;
      background: rgba(0,0,0,0.3);
      border-top: 1px solid #1a2550;
      padding: 0 20px; font-size: 13px; color: #ffd93d;
      overflow: hidden;
    }
    .footer .alert-tag {
      background: rgba(255,107,107,0.2);
      color: #ff6b6b; padding: 2px 8px;
      border-radius: 3px; font-size: 11px;
      margin-right: 10px; flex-shrink: 0;
    }
    .marquee {
      white-space: nowrap;
      animation: marquee 30s linear infinite;
    }
    @keyframes marquee {
      0% { transform: translateX(100%); }
      100% { transform: translateX(-100%); }
    }
  </style>
</head>
<body>

<div class="screen" id="screen">
  <!-- Header -->
  <div class="header">
    <span class="date-info">星期三</span>
    <h1>XX工厂 智能制造 OEE 看板</h1>
    <span class="clock" id="clock"></span>
  </div>

  <!-- KPI Cards -->
  <div class="kpi-row">
    <div class="kpi-card">
      <div class="kpi-label">设备综合效率 OEE</div>
      <div class="kpi-value cyan" id="kpi-oee">--</div>
      <div class="kpi-trend up">↑ 较昨日 +1.2%</div>
    </div>
    <div class="kpi-card">
      <div class="kpi-label">直通率 FPY</div>
      <div class="kpi-value green" id="kpi-fpy">--</div>
      <div class="kpi-trend up">↑ 较昨日 +0.3%</div>
    </div>
    <div class="kpi-card">
      <div class="kpi-label">计划达成率</div>
      <div class="kpi-value blue" id="kpi-plan">--</div>
      <div class="kpi-trend down">↓ 较昨日 -0.8%</div>
    </div>
    <div class="kpi-card">
      <div class="kpi-label">停机时长 (h)</div>
      <div class="kpi-value yellow" id="kpi-downtime">--</div>
      <div class="kpi-trend down">↓ 较昨日 +0.5h</div>
    </div>
  </div>

  <!-- Charts -->
  <div class="chart-grid">
    <div class="chart-box">
      <div class="chart-title">各产线产量 (件)</div>
      <div class="chart-container" id="chart-bar"></div>
    </div>
    <div class="chart-box">
      <div class="chart-title">综合 OEE 仪表盘</div>
      <div class="chart-container" id="chart-gauge"></div>
    </div>
    <div class="chart-box">
      <div class="chart-title">24h OEE 趋势</div>
      <div class="chart-container" id="chart-line"></div>
    </div>
    <div class="chart-box">
      <div class="chart-title">停机原因分布</div>
      <div class="chart-container" id="chart-pie"></div>
    </div>
  </div>

  <!-- Footer -->
  <div class="footer">
    <span class="alert-tag">告警</span>
    <span class="marquee">
      [14:25] 3号线焊装工位温度异常 (82.3°C) &nbsp;|&nbsp;
      [14:18] 1号线 CNC-05 刀具磨损预警 &nbsp;|&nbsp;
      [13:52] B车间能效偏低 (PUE=1.65) &nbsp;|&nbsp;
      [13:30] 2号线AOI检测良率下降至94.2%
    </span>
  </div>
</div>

<script>
// ===== 1. Scale 自适应 =====
function autoScale() {
  const el = document.getElementById('screen');
  const sx = window.innerWidth / 1920;
  const sy = window.innerHeight / 1080;
  const s = Math.min(sx, sy);
  el.style.transform = `scale(${s})`;
  el.style.left = (window.innerWidth - 1920 * s) / 2 + 'px';
  el.style.top = (window.innerHeight - 1080 * s) / 2 + 'px';
}
window.addEventListener('resize', autoScale);
autoScale();

// ===== 2. 实时时钟 =====
function updateClock() {
  const now = new Date();
  const pad = n => String(n).padStart(2, '0');
  document.getElementById('clock').textContent =
    `${now.getFullYear()}-${pad(now.getMonth()+1)}-${pad(now.getDate())} `
    + `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
}
setInterval(updateClock, 1000);
updateClock();

// ===== 3. 数字滚动动画 =====
function animateNum(el, start, end, dur, dec, suffix='%') {
  const t0 = performance.now();
  (function step(t) {
    const p = Math.min((t - t0) / dur, 1);
    const ease = 1 - Math.pow(2, -10 * p);
    el.textContent = (start + (end - start) * ease).toFixed(dec) + suffix;
    if (p < 1) requestAnimationFrame(step);
  })(t0);
}
animateNum(document.getElementById('kpi-oee'), 0, 85.2, 2000, 1);
animateNum(document.getElementById('kpi-fpy'), 0, 96.8, 2000, 1);
animateNum(document.getElementById('kpi-plan'), 0, 92.1, 2000, 1);
animateNum(document.getElementById('kpi-downtime'), 0, 2.5, 2000, 1, 'h');

// ===== 4. ECharts 通用配色 =====
const COLORS = ['#4facfe', '#00f2fe', '#22c55e', '#ffd93d',
               '#d2a8ff', '#ff6b6b', '#06b6d4', '#f97316'];

// ===== 5. 柱状图:各产线产量 =====
const barChart = echarts.init(document.getElementById('chart-bar'));
barChart.setOption({
  tooltip: { trigger: 'axis' },
  grid: { left: 50, right: 20, top: 30, bottom: 30 },
  xAxis: {
    type: 'category',
    data: ['1号线', '2号线', '3号线', '4号线', '5号线', '6号线'],
    axisLine: { lineStyle: { color: '#1a2550' } },
    axisLabel: { color: '#bac2de' }
  },
  yAxis: {
    type: 'value',
    splitLine: { lineStyle: { color: '#0f1535' } },
    axisLabel: { color: '#bac2de' }
  },
  series: [{
    type: 'bar',
    data: [1280, 1156, 1342, 1089, 1198, 1245],
    barWidth: '40%',
    itemStyle: {
      borderRadius: [4, 4, 0, 0],
      color: new echarts.graphic.LinearGradient(0,0,0,1,[
        {offset:0, color:'#4facfe'},
        {offset:1, color:'#00f2fe22'}
      ])
    }
  }]
});

// ===== 6. 仪表盘:综合 OEE =====
const gaugeChart = echarts.init(document.getElementById('chart-gauge'));
gaugeChart.setOption({
  series: [{
    type: 'gauge',
    startAngle: 220, endAngle: -40,
    min: 0, max: 100,
    radius: '85%',
    progress: { show: true, width: 14,
      itemStyle: { color: '#00f2fe' } },
    axisLine: { lineStyle: { width: 14, color: [[1, '#1a2550']] } },
    axisTick: { show: false },
    splitLine: { show: false },
    axisLabel: { distance: 20, color: '#bac2de', fontSize: 11 },
    pointer: { show: false },
    title: { offsetCenter: [0, '70%'], color: '#bac2de', fontSize: 13 },
    detail: {
      valueAnimation: true,
      offsetCenter: [0, '35%'],
      fontSize: 36, fontWeight: 'bold',
      color: '#00f2fe',
      formatter: '{value}%'
    },
    data: [{ value: 85.2, name: '综合 OEE' }]
  }]
});

// ===== 7. 折线图:24h OEE 趋势 =====
const hours = Array.from({length:24}, (_,i) => `${String(i).padStart(2,'0')}:00`);
const oeeData = [78,76,75,74,73,72,80,85,87,88,
  86,84,83,85,86,88,87,86,85,83,82,80,79,78];
const lineChart = echarts.init(document.getElementById('chart-line'));
lineChart.setOption({
  tooltip: { trigger: 'axis' },
  grid: { left: 45, right: 15, top: 20, bottom: 30 },
  xAxis: {
    type: 'category', data: hours, boundaryGap: false,
    axisLine: { lineStyle: { color: '#1a2550' } },
    axisLabel: { color: '#bac2de', fontSize: 10,
      interval: 3 }
  },
  yAxis: {
    type: 'value', min: 60, max: 100,
    splitLine: { lineStyle: { color: '#0f1535' } },
    axisLabel: { color: '#bac2de' }
  },
  series: [{
    type: 'line', data: oeeData, smooth: true,
    symbol: 'none', lineStyle: { color: '#d2a8ff', width: 2 },
    areaStyle: {
      color: new echarts.graphic.LinearGradient(0,0,0,1,[
        {offset:0, color:'rgba(168,85,247,0.3)'},
        {offset:1, color:'rgba(168,85,247,0)'}
      ])
    }
  }]
});

// ===== 8. 饼图:停机原因分布 =====
const pieChart = echarts.init(document.getElementById('chart-pie'));
pieChart.setOption({
  tooltip: { trigger: 'item' },
  legend: {
    orient: 'vertical', right: 10, top: 'center',
    textStyle: { color: '#bac2de', fontSize: 11 }
  },
  series: [{
    type: 'pie', radius: ['40%', '70%'],
    center: ['35%', '50%'],
    label: { show: false },
    data: [
      { value: 35, name: '设备故障', itemStyle: {color:'#ff6b6b'} },
      { value: 25, name: '换线调整', itemStyle: {color:'#ffd93d'} },
      { value: 20, name: '物料等待', itemStyle: {color:'#4facfe'} },
      { value: 12, name: '计划停机', itemStyle: {color:'#22c55e'} },
      { value: 8,  name: '其他', itemStyle: {color:'#d2a8ff'} }
    ]
  }]
});

// ===== 9. 窗口缩放自适应 =====
window.addEventListener('resize', () => {
  barChart.resize();
  gaugeChart.resize();
  lineChart.resize();
  pieChart.resize();
});
</script>
</body>
</html>
5

性能优化

图表实例管理 — 避免内存泄漏
类比理解

ECharts 实例就像你雇的工人。每创建一个实例 = 雇一个工人。
如果你不辞退(dispose)离职的工人,他们还在领工资(占内存)。
一个大屏 6-8 个图表 = 6-8 个工人,不多。但如果你在 SPA 路由切换中反复创建不销毁,内存就爆了。

chart-manager.js — 图表实例统一管理JavaScript
// 图表管理器:统一创建、更新、销毁
const ChartManager = {
  // 存储所有图表实例
  instances: new Map(),

  // 创建或获取已有实例
  getOrCreate(containerId) {
    if (this.instances.has(containerId)) {
      return this.instances.get(containerId);
    }
    const dom = document.getElementById(containerId);
    const chart = echarts.init(dom);
    this.instances.set(containerId, chart);
    return chart;
  },

  // 更新数据(不重建实例)
  updateOption(containerId, option) {
    const chart = this.getOrCreate(containerId);
    chart.setOption(option, { notMerge: false });
  },

  // 全部 resize
  resizeAll() {
    this.instances.forEach(chart => chart.resize());
  },

  // 销毁所有(页面卸载时调用)
  disposeAll() {
    this.instances.forEach(chart => chart.dispose());
    this.instances.clear();
  }
};

// 使用示例
ChartManager.updateOption('chart-bar', barOption);

// 页面销毁时
window.addEventListener('beforeunload', () => {
  ChartManager.disposeAll();
});
🔄 数据节流 + Resize 防抖
throttle-debounce.jsJavaScript
// 1. 数据轮询节流:如果上一次请求还没回来,不发起下一次
let isFetching = false;
setInterval(async () => {
  if (isFetching) return; // 跳过
  isFetching = true;
  try {
    const data = await fetchData();
    updateDashboard(data);
  } finally {
    isFetching = false;
  }
}, 10000);

// 2. Resize 防抖:避免窗口拖拽时疯狂 resize
function debounce(fn, delay) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}

const debouncedResize = debounce(() => {
  autoScale();
  ChartManager.resizeAll();
}, 200);

window.addEventListener('resize', debouncedResize);

// 3. requestAnimationFrame 优化动画
// 大量 DOM 动画放在 rAF 中,与浏览器刷新率同步
function smoothUpdate(el, newValue) {
  requestAnimationFrame(() => {
    el.textContent = newValue;
  });
}
优化项做法效果
图表过多用 ChartManager 统一管理,避免重复 init减少内存占用
频繁更新setOption 时用 notMerge: false,只更新数据避免整个图表重绘
数据量大折线图开启 large: true,大数据模式万级数据不卡顿
定时器泄漏页面卸载前 clearInterval + dispose避免内存泄漏
窗口 resize200ms 防抖,避免高频触发CPU 占用降低
6

部署流程

🚀 从开发到上墙的完整流程
本地开发
打包构建
Nginx 配置
部署到服务器
大屏 TV 访问
  1. 1. 本地开发:在 localhost:8080 开发调试,确认所有图表正常渲染、数据轮询正常
  2. 2. 打包构建npm run build 或直接把 HTML/CSS/JS 文件放到 dist/ 目录
  3. 3. 服务器部署:把 dist/ 上传到服务器,配置 Nginx
  4. 4. 大屏 TV:用 TV 内置浏览器或连接迷你主机,全屏打开大屏 URL
Nginx 配置示例nginx.conf
# /etc/nginx/conf.d/dashboard.conf
server {
    listen       80;
    server_name  dashboard.factory.local;

    # 静态文件
    root  /usr/share/nginx/html/dashboard;
    index index.html;

    # SPA 路由 — 所有路径都返回 index.html
    location / {
        try_files $uri $uri/ /index.html;
    }

    # API 反向代理
    location /api/ {
        proxy_pass http://192.168.1.100:3000/;
        proxy_set_header Host $host;
    }

    # 缓存静态资源(大屏不变的前端文件)
    location ~* \.(js|css|png|jpg|woff2)$ {
        expires 7d;
        add_header Cache-Control "public, immutable";
    }
}
一键部署脚本Shell
# deploy.sh — 一键部署大屏到生产服务器
# 用法: ./deploy.sh

# 1. 打包
npm run build

# 2. 压缩
tar -czf dist.tar.gz dist/

# 3. 上传到服务器
scp dist.tar.gz root@192.168.1.200:/tmp/

# 4. 远程解压部署
ssh root@192.168.1.200 "cd /usr/share/nginx/html/dashboard \
  && rm -rf * \
  && tar -xzf /tmp/dist.tar.gz --strip-components=1 \
  && nginx -s reload"

echo "部署完成! 访问: http://dashboard.factory.local"
大屏 TV 小技巧: 浏览器全屏:Chrome 按 F11 进入全屏模式(kiosk 模式启动:chrome --kiosk URL)。
自动刷新:每天凌晨自动刷新页面,防止长时间运行导致内存泄漏 → setTimeout(() => location.reload(), 24*3600*1000)
7

面试考点 — Top 6 大屏问题

1大屏适配方案有哪些?你用哪种?
三种方案:
(1) scale 缩放(推荐):固定 1920×1080 设计,transform: scale(ratio) 等比缩放。优点是像素级还原、最简单。
(2) rem 方案:动态设置 html font-size,所有尺寸用 rem。灵活但需要换算。
(3) vw/vh:用视口单位。简单但非等比屏幕会变形。

回答模板:「我们项目用的是 scale 方案。因为大屏 TV 都是标准 16:9 分辨率,scale 最简单稳定。核心代码就是计算 scaleX 和 scaleY 取最小值,设置 transform。」
2如何实现实时数据更新?
两种方案:
(1) setInterval 轮询:每 10-30 秒请求一次 API,适合大多数制造业场景(OEE、产量不需要秒级更新)。
(2) WebSocket:服务端主动推送,适合告警场景(设备故障需要秒级通知)。

实际项目:通常两者结合 — 轮询更新 KPI 数据,WebSocket 推送告警。轮询要加 请求锁(防止上一次请求未完成就发起下一次),页面卸载时 clearInterval 防止内存泄漏。
3图表太多(10+ 个)如何优化性能?
(1) 统一管理:用 ChartManager 模式,Map 存储所有实例,避免重复 init。
(2) 懒初始化:不在 mounted 时一次性 init 所有图表,用 IntersectionObserver 在图表进入可视区域时再 init。
(3) 增量更新setOption(option, { notMerge: false }) 只更新变化的数据,不整个重绘。
(4) 大数据开启 large 模式:ECharts 的 large: true 模式用 Canvas 批量绘制,万级数据也不卡。
(5) 降采样:折线图数据超过 500 个点时,前端做降采样(取最大最小值),减少渲染压力。
4数据量大时(万级)前端怎么处理?
(1) 前端分页/聚合:请求时让后端做聚合(如每小时一个数据点),不要把原始秒级数据全拉到前端。
(2) ECharts large 模式series.large = true,底层用 Canvas 批量绘制。
(3) 数据窗口:用 dataZoom 组件,只渲染当前可视范围的数据。
(4) Web Worker:大量数据计算(排序、聚合)放到 Worker 线程,不阻塞主线程。
(5) 虚拟列表:如果是表格展示,用虚拟滚动只渲染可见行。
5大屏刷新策略怎么做?
(1) 定时刷新:不同模块不同频率。KPI 数字 10 秒刷新,图表 30 秒刷新,告警实时推送。
(2) 避免白屏:刷新时先合并数据再渲染,不要先清空再填充。
(3) 数字动画:用 requestAnimationFrame 做 countUp 动画,从旧值平滑过渡到新值。
(4) 每日重载:大屏 7×24 运行,每天凌晨自动 location.reload(),防止长时间运行导致的内存泄漏。
(5) 断线处理:请求失败时显示上次数据 + 「数据同步中」提示,不要整个大屏报错。
6大屏开发的完整流程?从需求到上墙
完整流程 7 步:
(1) 需求确认:和业务方确认展示哪些指标(OEE、直通率、停机时长等)、数据来源(MES/SCADA/ERP)。
(2) UI 设计:UI 设计师出 1920×1080 设计稿(Figma),确认布局、配色、图表类型。
(3) 技术选型:纯 HTML + ECharts(简单大屏)或 Vue + ECharts(复杂大屏)。
(4) 前端开发:scale 适配 + 暗色主题 + 图表开发 + 数据轮询 + 数字动画。
(5) 后端接口:开发 API 接口,返回 KPI 数据、图表数据、告警数据。
(6) 联调测试:对接真实数据,验证数据正确性、刷新稳定性。
(7) 部署上墙:Nginx 部署 + 大屏 TV 全屏浏览 + kiosk 模式 + 每日自动刷新。
面试加分回答:「我们之前的大屏项目,还做了 数据脱敏(大屏放在公共区域,敏感数据做了脱敏处理)和 多主题切换(白天/夜晚两种亮度模式,适应不同光线环境)。」

可视化大屏开发实战 — 制造业数据大屏全攻略

ECharts + Scale 适配 + 暗色主题 + 实时数据 + 数字动画 + 性能优化 + Nginx 部署