一、方案概述

1.1 方案定位

本方案旨在基于Vue3 + Node.js全栈技术,从零构建一套全功能的工业Web HMI系统。不同于购买商业组态软件或直接套用开源框架,本方案强调自主掌控核心代码、完全定制化功能、与现有业务系统深度融合的能力,适用于设备制造商构建自有品牌的差异化HMI产品、系统集成商开发定制化工业监控平台、以及有特定行业需求的终端用户自建监控体系。

1.2 Web HMI的行业趋势

Web HMI正加速成为工业可视化的主流方向。西门子WinCC Unified全面采用HTML5、SVG和JavaScript等原生Web技术构建可视化系统,支持通过任何现代Web浏览器访问。力控科技推出FWebSCADA云组态监控平台,采用分层式架构(设备接入层→数据处理层→应用服务层→网络传输层→用户交互层),依托Web技术实现免客户端安装、多平台操作。N3uron的Web Vision模块是纯Web的HMI/SCADA界面,完全基于JavaScript、HTML5和CSS技术,支持智能手机、平板、触摸屏、笔记本电脑和控制室视频墙等全设备类型。

传统HMI的根本缺陷在于封闭的架构——依赖专用硬件终端、需要安装特定运行环境和驱动、缺乏原生远程访问能力,导致设备数据成为“孤岛”。基于Web的HMI则具备更高的灵活性和可扩展性,正在从根本上改变工业可视化的部署和运维模式。

在这里插入图片描述

二、技术栈选型详解

2.1 前端技术栈

核心框架:Vue 3.4+(Composition API + TypeScript)

Vue3的Composition API提供更好的逻辑复用和代码组织能力,TypeScript确保大型项目的类型安全和可维护性。参考成熟的工业可视化项目,Vue3已被广泛验证适用于工业监控场景。

构建工具:Vite 5.x

Vite提供极速冷启动和热更新体验,适合大型HMI工程的开发效率要求。

状态管理:Pinia 2.x

Vue官方推荐的状态管理库,轻量且类型安全,适合管理HMI中的设备状态、报警队列、用户会话等全局数据。

UI组件库:Element Plus / Ant Design Vue

提供成熟的表单、表格、弹窗、菜单等通用UI组件,加速非核心界面的开发。二者均为Vue3生态中TypeScript优先、活跃维护的官方推荐组件库。

数据可视化核心引擎:ECharts 5.x

处理HMI核心的实时趋势曲线、柱状图、饼图、仪表盘等数据可视化需求。ECharts支持大规模数据的高性能渲染和动态数据更新。

Web组态编辑器:自研 + SVG/Canvas

基于Vue3的SVG可视化Web组态编辑器可实现工业控制系统的拓扑图、流程图设计,通过拖拽方式添加SVG元素,并利用Vue3的响应式特性实现数据动态绑定。开源方案如maotu-webtopo提供了将SVG文件转换为可自由缩放、交互的图形化组件库的参考实现,支持低代码拖拽开发。

其他关键依赖:

依赖 版本 用途
Vue Router 4.x 画面导航与路由
Axios 1.x HTTP通信
Day.js 1.x 时间处理
GSAP 3.x 高级动画效果
Three.js 0.170+ 3D设备模型渲染(可选)

2.2 后端技术栈

运行环境:Node.js 20 LTS + TypeScript

Node.js的事件驱动和非阻塞I/O模型天然适合处理大量并发的工业设备连接。

Web框架:NestJS / Express + Socket.IO

NestJS的模块化架构适合构建大型后端服务,提供依赖注入、中间件、守卫等企业级特性。若团队倾向轻量方案,Express配合中间件模式同样可行。

实时通信:Socket.IO 4.x / 原生WebSocket

Web组态本质上通过HTML5+WebGL技术栈实现浏览器端工业控制界面开发,关键技术突破在于实现了低延迟数据通信(WebSocket协议)与高精度图形渲染(Canvas/WebGL)的融合。Socket.IO在WebSocket基础上提供自动重连、房间管理、事件命名空间等增强能力。

协议驱动层:

协议 驱动方案 说明
Modbus TCP modbus-serial 最通用的工业协议
Modbus RTU modbus-serial + serialport 串口通信支持
OPC UA node-opcua 高级数据互通标准
MQTT mqtt.js 物联网/云平台连接
Siemens S7 nodes7 / plc.js 西门子PLC直连

FUXA等成熟开源项目已验证了上述驱动在Node.js生态中的可行性——其设备连接层支持OPC UA、Modbus、MQTT、西门子S7等多种工业协议,并通过Node.js后端实现与PLC、传感器等设备的实时数据交互。

数据存储:

场景 方案 说明
时序数据 InfluxDB / TimescaleDB 历史趋势和报警归档
配置数据 PostgreSQL / MySQL 用户、权限、配方、工程文件
缓存/会话 Redis 实时数据缓存、会话管理
本地轻量 SQLite 单机部署方案

2.3 打包部署方案

Web部署(标准模式):

前端Vue3项目通过Vite打包为静态资源,部署至Nginx或CDN;后端Node.js服务通过PM2进程守护运行,支持Docker容器化部署。FUXA即采用此模式,支持Windows/Linux/Docker跨平台部署。

桌面端部署(Tauri模式):

对于需要在嵌入式或工控机上独立运行的场景,可采用Tauri方案。Tauri使用系统原生WebView渲染前端,Rust编写后端,打包体积小(<10MB)、内存占用低。已有专为工业HMI应用打造的Vue3 + Tauri库,提供开箱即用的Modbus通信指令、数据操作API和工业级UI组件。参考架构:前端Vue3通过invoke调用Tauri后端的Rust命令,Rust后端封装Modbus/OPC UA等协议的通信逻辑。

三、系统架构设计

3.1 分层架构总览

┌─────────────────────────────────────────────────────────┐
│                    展示交互层(Presentation)              │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐ │
│  │ 监控画面 │  │ 报警管理 │  │ 趋势分析 │  │ 报表中心 │ │
│  │ (组态渲染)│  │ (实时推送)│  │ (ECharts)│  │ (导出)   │ │
│  └──────────┘  └──────────┘  └──────────┘  └──────────┘ │
│  ┌────────────────────────────────────────────────────┐ │
│  │        Web组态编辑器(拖拽设计、SVG组件库)          │ │
│  └────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────┤
│                    业务逻辑层(Application)              │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐ │
│  │ 用户权限 │  │ 报警引擎 │  │ 配方管理 │  │ 脚本引擎 │ │
│  │ (RBAC)   │  │ (规则判断)│  │ (版本控制)│  │ (JS沙箱) │ │
│  └──────────┘  └──────────┘  └──────────┘  └──────────┘ │
├─────────────────────────────────────────────────────────┤
│                    数据服务层(Data Service)              │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐ │
│  │ 实时数据 │  │ 历史归档 │  │ 数据缓存 │  │ API网关  │ │
│  │ (推送)   │  │ (压缩存储)│  │ (Redis)  │  │ (REST)   │ │
│  └──────────┘  └──────────┘  └──────────┘  └──────────┘ │
├─────────────────────────────────────────────────────────┤
│                    设备接入层(Device Connectivity)       │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐ │
│  │ Modbus   │  │ OPC UA   │  │ MQTT     │  │ S7       │ │
│  │ TCP/RTU  │  │ Client   │  │ Broker   │  │ Client   │ │
│  └──────────┘  └──────────┘  └──────────┘  └──────────┘ │
│  ┌────────────────────────────────────────────────────┐ │
│  │           协议适配器(Protocol Adapter)             │ │
│  │   统一数据模型抽象,新协议热插拔                     │ │
│  └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘

3.2 数据流设计

实时数据上行(设备→HMI):

PLC/传感器 → 协议驱动 → 数据解析 → Redis缓存 + InfluxDB归档
                                      ↓
                               WebSocket推送 → Vue3响应式更新 → 画面刷新

控制指令下行(HMI→设备):

用户操作 → 权限校验 → 指令队列 → 协议驱动 → 设备执行
                ↓
           操作日志 → 审计追踪

3.3 目录结构规划

web-hmi-platform/
├── packages/
│   ├── frontend/                 # Vue3前端
│   │   ├── src/
│   │   │   ├── views/           # 画面(监控、报警、趋势、报表)
│   │   │   ├── components/      # 公共组件
│   │   │   │   ├── hmi/         # HMI核心组件(仪表、按钮、指示灯)
│   │   │   │   ├── charts/      # 图表组件(趋势图、仪表盘)
│   │   │   │   └── editor/      # 组态编辑器组件
│   │   │   ├── composables/     # 组合式函数(useWebSocket、useTag等)
│   │   │   ├── stores/          # Pinia状态管理
│   │   │   ├── router/          # 路由配置
│   │   │   ├── assets/          # 静态资源与SVG图元库
│   │   │   └── types/           # TypeScript类型定义
│   │   └── vite.config.ts
│   │
│   ├── backend/                  # Node.js后端
│   │   ├── src/
│   │   │   ├── modules/
│   │   │   │   ├── device/      # 设备管理(协议驱动、连接池)
│   │   │   │   ├── tag/         # 标签管理(变量定义、映射)
│   │   │   │   ├── alarm/       # 报警引擎
│   │   │   │   ├── recipe/      # 配方管理
│   │   │   │   ├── history/     # 历史数据服务
│   │   │   │   ├── auth/        # 认证授权
│   │   │   │   └── report/      # 报表服务
│   │   │   ├── common/          # 公共工具、中间件、守卫
│   │   │   └── config/          # 配置文件
│   │   └── package.json
│   │
│   └── shared/                   # 前后端共享类型定义
│       └── types/
│
├── docker/                       # Docker部署配置
├── docs/                         # 技术文档
└── package.json                  # Monorepo根配置

在这里插入图片描述

四、完整功能模块需求

4.1 功能模块全景图

本全功能HMI系统涵盖以下十二大功能模块

模块 核心功能 优先级
组态编辑器 可视化画面设计、拖拽布局、SVG图元库、属性绑定 P0
实时监控 设备状态、过程变量、动态画面、动画效果 P0
报警管理 报警规则、分级分类、实时推送、确认处理、归档查询 P0
趋势分析 实时趋势、历史趋势、多曲线对比、缩放与导出 P0
设备通信 多协议驱动、连接管理、断线重连、数据映射 P0
用户权限 角色管理、操作权限、电子签名、审计追踪 P1
配方管理 配方编辑、版本管理、下发与上传、对比 P1
报表中心 模板设计、定时生成、多格式导出、自动推送 P1
数据归档 历史数据存储、压缩、查询、备份恢复 P1
系统管理 工程管理、备份恢复、系统日志、许可管理 P1
移动适配 响应式布局、移动端APP壳、消息推送 P2
扩展集成 REST API、OPC UA Server、MQTT桥接、第三方对接 P2

4.2 各模块详细需求

4.2.1 组态编辑器(核心差异化模块)

组态编辑器是Web HMI区别于简单大屏的关键——使用者无需编码即可创建监控画面。

功能需求:

  • 图元库管理: 提供100+工业标准图元(阀门、泵、管道、电机、储罐、传感器、仪表盘),支持SVG格式自定义导入
  • 拖拽式布局: 画布上自由拖拽、缩放、旋转、对齐、分布组件,支持标尺、网格吸附、图层管理、多选(鼠标框选)、CTRL+C/V复制粘贴等精确操控能力
  • 属性面板: 选中组件后在右侧面板调整颜色、大小、动画、数据绑定等属性
  • 数据绑定: 将图元的颜色、位置、文本、可见性等属性与后端标签(Tag)动态绑定,颜色随设备状态变化
  • 交互配置: 为组件配置点击事件——画面跳转、弹出窗口、远程开门/开关阀门等控制指令下发(操作前需进行权限校验与合法性验证)
  • 画面管理: 支持多层级画面结构(总览→区域→设备→参数)、画面模板复用
  • 保存与导出: 支持将设计保存为JSON文件或导出为SVG格式

参考开源实现phynos/WebTopo,其组件系统无缝集成了传统DOM组件(文本、图片)、自定义Canvas组件(矩形、箭头)和外部可视化库(如ECharts),所有组件在同一可编辑画布内协同工作。

4.2.2 实时监控
  • 支持毫秒级至100ms级数据刷新,通过WebSocket实现服务端主动推送
  • 画面自适应不同分辨率(1920×1080大屏至1024×768工控机),采用动态屏幕适配方案
  • 导航逻辑支持总览→区域→设备→参数的逐级钻取
  • 画面间切换动画过渡,操作反馈视觉提示
4.2.3 报警管理
  • 支持多种报警类型:限值报警(高/高高/低/低低)、偏差报警、变化率报警、开关量报警
  • 报警级别至少4级(紧急/严重/警告/提示)
  • 报警发生时自动弹窗、语音播报、画面闪烁、邮件/短信推送
  • 报警确认支持单条确认和批量确认,确认操作关联用户信息和时间戳
  • 报警归档支持按时间、级别、设备等维度多条件组合查询
4.2.4 设备通信
  • 支持同时连接多个不同类型设备,每个设备独立处理循环,互不阻塞
  • 连接池管理:每个设备独立连接,支持自动重连、故障恢复、心跳检测
  • 统一标签(Tag)数据模型:屏蔽底层协议差异,上层只读写标签
  • 标签属性包括:名称、地址(协议相关)、数据类型、读写权限、采集周期、缩放系数、死区等
  • 事件驱动架构实现实时状态更新和数据同步
4.2.5 用户权限与安全
  • RBAC角色权限模型:至少支持操作员、工艺员、管理员三级角色
  • 关键操作(配方修改、参数变更、报警确认)需电子签名
  • 操作日志满足审计追踪要求
  • 通信安全:HTTPS/WSS加密、TLS证书认证
  • API接口需进行身份认证与权限校验

五、全栈开发具体实施步骤

5.1 阶段划分总览

阶段 周期(参考) 里程碑交付
第一阶段:基础框架 第1-3周 项目脚手架、前后端通信、数据库模型、基础UI布局
第二阶段:设备通信 第4-6周 Modbus/OPC UA驱动集成、标签管理、实时数据推送
第三阶段:组态编辑器 第7-10周 图元库、拖拽画布、属性绑定、画面管理
第四阶段:核心功能 第11-14周 报警引擎、趋势曲线、配方管理、用户权限
第五阶段:完善增强 第15-17周 报表中心、数据归档、移动适配、性能优化
第六阶段:测试部署 第18-20周 集成测试、压力测试、文档编写、生产部署

5.2 第一阶段:基础框架搭建

步骤1:项目初始化

# 创建Monorepo根目录
mkdir web-hmi-platform && cd web-hmi-platform
pnpm init

# 创建前端项目
pnpm create vite packages/frontend --template vue-ts
cd packages/frontend
pnpm add vue-router@4 pinia axios dayjs element-plus echarts
pnpm add -D @types/node sass unplugin-auto-import unplugin-vue-components

# 创建后端项目
mkdir -p packages/backend/src
cd packages/backend
pnpm init
pnpm add express socket.io cors helmet jsonwebtoken bcryptjs
pnpm add better-sqlite3 sequelize pg ioredis
pnpm add modbus-serial node-opcua mqtt
pnpm add -D typescript @types/node @types/express ts-node nodemon
npx tsc --init

步骤2:前端基础架构

packages/frontend/src/下建立核心结构——配置Vue Router画面导航、Pinia全局状态管理、Axios HTTP封装、以及基于Element Plus的主布局框架(侧边栏画面导航 + 顶部状态栏 + 主内容区)。同时定义共享TypeScript类型文件(Tag、Alarm、Device等核心实体类型)。

步骤3:后端基础架构

搭建Express/NestJS服务框架,实现WebSocket服务(基于Socket.IO)、JWT认证中间件、统一响应格式封装和全局错误处理。配置数据库连接(开发环境使用SQLite,生产环境切换PostgreSQL)。

5.3 第二阶段:设备通信层

这是HMI系统的核心基础。设备通信层负责与PLC、传感器等工业设备建立连接、收发数据。

步骤1:协议适配器架构设计

定义IProtocolAdapter接口,统一不同协议的接入方式:

// packages/backend/src/modules/device/protocols/adapter.interface.ts
export interface IProtocolAdapter {
  readonly protocol: string
  connect(config: DeviceConfig): Promise<void>
  disconnect(): void
  read(tags: TagDefinition[]): Promise<Record<string, any>>
  write(tagName: string, value: any): Promise<void>
  isConnected(): boolean
  onData(callback: (data: Record<string, any>) => void): void
  onError(callback: (error: Error) => void): void
}

步骤2:实现Modbus TCP驱动

// packages/backend/src/modules/device/protocols/modbus-tcp.adapter.ts
import ModbusRTU from 'modbus-serial'
import { IProtocolAdapter } from './adapter.interface'
import { EventEmitter } from 'events'

export class ModbusTCPAdapter extends EventEmitter implements IProtocolAdapter {
  readonly protocol = 'modbus-tcp'
  private client: ModbusRTU
  private pollingTimer: NodeJS.Timeout | null = null
  private connected = false

  constructor() {
    super()
    this.client = new ModbusRTU()
  }

  async connect(config: DeviceConfig): Promise<void> {
    await this.client.connectTCP(config.host, { port: config.port || 502 })
    this.client.setID(config.unitId || 1)
    this.client.setTimeout(3000)
    this.connected = true
  }

  async read(tags: TagDefinition[]): Promise<Record<string, any>> {
    const results: Record<string, any> = {}
    for (const tag of tags) {
      const addr = parseInt(tag.address)
      if (tag.registerType === 'holding') {
        const { data } = await this.client.readHoldingRegisters(addr, tag.length || 1)
        results[tag.name] = this.convertValue(data, tag.dataType)
      } else if (tag.registerType === 'coil') {
        const { data } = await this.client.readCoils(addr, 1)
        results[tag.name] = data[0] === 1
      }
    }
    return results
  }

  async write(tagName: string, value: any): Promise<void> {
    const tag = this.getTagDefinition(tagName)
    const addr = parseInt(tag.address)
    if (tag.registerType === 'holding') {
      await this.client.writeRegister(addr, value)
    } else if (tag.registerType === 'coil') {
      await this.client.writeCoil(addr, value ? 1 : 0)
    }
  }

  async disconnect(): Promise<void> {
    if (this.pollingTimer) clearInterval(this.pollingTimer)
    this.client.close(() => {})
    this.connected = false
  }

  isConnected(): boolean { return this.connected }

  startPolling(tags: TagDefinition[], interval: number = 500): void {
    this.pollingTimer = setInterval(async () => {
      try {
        const data = await this.read(tags)
        this.emit('data', data)
      } catch (err) {
        this.emit('error', err)
      }
    }, interval)
  }
}

步骤3:标签管理与实时数据推送

建立标签配置数据库模型,启动时加载所有设备配置并建立连接。设备读取数据后,数据进入Redis缓存层(热数据),同时通过WebSocket推送到前端。前端通过useWebSocket组合式函数接收数据,更新Pinia Store中的实时数据,画面中的绑定组件自动响应式刷新。

5.4 第三阶段:组态编辑器

步骤1:图元组件库

创建基于SVG的工业图元组件库,每种图元是一个Vue组件,接收统一的props(value值决定颜色/状态、animated控制动画、label标签文本):

// 图元组件接口
interface SymbolProps {
  uid: string
  value: any
  config: SymbolConfig   // 颜色、动画、缩放等配置
  interactive: boolean   // 编辑模式/运行模式
  events?: SymbolEvents  // 点击等交互事件
}

步骤2:画布编辑器

画布使用CSS Transform实现缩放和平移。使用vue-draggable-resizable或自行实现组件的拖拽、缩放、旋转手柄,支持标尺辅助线和网格吸附。图元面板从组件库中加载所有可用图元,拖入画布后生成组件实例。属性面板根据选中组件类型动态渲染编辑表单(颜色选择器、尺寸调整、数据绑定配置)。

步骤3:数据绑定与画面保存

在属性面板中配置数据绑定:选择标签→选择属性→选择变换规则(如值>80时红色,否则绿色)。编辑完成的画面序列化为JSON格式存储到后端,JSON格式包含画布尺寸、背景、组件列表及其属性。

5.5 第四阶段:核心功能开发

报警引擎:

后端定义报警规则模型(Tag引用、条件类型、阈值、级别、消息模板)。数据采集后,每条数据经过规则引擎评估,命中规则时生成报警记录(报警时间、级别、Tag名称、当前值、消息、确认状态),报警记录写入数据库并通过WebSocket实时推送到前端。

趋势曲线:

后端提供历史数据查询API——按标签和时间范围查询时序数据,支持聚合降采样(原始数据→平均值/最大值/最小值,适用于长期数据查询)。前端使用ECharts渲染,支持实时趋势滚动刷新和历史趋势时间范围选择、多Y轴、缩放漫游。

配方管理:

数据库设计配方模型(配方ID、名称、版本、创建时间、参数列表JSON)。前端提供配方编辑器(表格形式编辑参数→命名→保存),配方下发时逐条将参数值写入对应PLC地址。

用户权限:

实现JWT认证(登录颁发Token、请求验证Token、过期刷新),RBAC权限模型(定义角色→为角色分配画面和操作权限→创建用户时关联角色→前端根据权限控制菜单和按钮显隐),关键操作写入审计日志。

5.6 第五阶段:完善增强

  • 报表中心: 使用模板引擎(如EJS/Pug)定义报表模板→查询数据→渲染HTML→通过Puppeteer生成PDF→定时任务自动发送邮件
  • 移动适配: 使用CSS媒体查询和弹性布局适配移动端尺寸;通过Capacitor打包为Android/iOS APP
  • 性能优化: 大量标签采用虚拟滚动渲染画面;使用Web Worker处理数据转换;为设备数据添加缓存层

5.7 第六阶段:测试与部署

测试策略:

测试类型 工具 覆盖范围
单元测试 Vitest + Jest 工具函数、协议解析、报警引擎逻辑
组件测试 Vue Test Utils 图元组件、表单组件
E2E测试 Playwright 画面导航、报警流程、配方下发
性能测试 k6 / wrk WebSocket并发连接、数据推送吞吐

部署方案:

  • 单机部署: PM2进程守护 + Nginx反向代理
  • Docker部署: 编写docker-compose.yml,含前端Nginx容器、后端Node.js容器、Redis容器、PostgreSQL容器、InfluxDB容器
  • 边缘部署: 前端打包为Tauri桌面应用,后端使用SQLite,整体打包为单个可执行文件

六、全栈开发示例:温度监控场景从零到运行

6.1 场景描述

某注塑机需要监控三个温度点(料筒温度、喷嘴温度、模具温度),每个温度点设置高报警阈值。操作人员需要在HMI上实时查看温度数值和趋势曲线,超温时自动弹窗报警并记录。

6.2 后端实现

定义标签和报警规则:

// packages/backend/src/modules/tag/tag.manager.ts
import { EventEmitter } from 'events'

interface TagConfig {
  name: string; description: string; deviceId: string
  address: string; dataType: string; registerType: string
  scanRate: number; alarmHigh?: number; alarmHighHigh?: number
}

interface AlarmRecord {
  id: string; tagName: string; alarmType: string
  alarmLevel: string; currentValue: number; threshold: number
  message: string; timestamp: Date; acknowledged: boolean
}

export class TagManager extends EventEmitter {
  private tags: Map<string, TagConfig> = new Map()
  private values: Map<string, any> = new Map()
  private alarms: AlarmRecord[] = []

  // ========== 标签配置管理 ==========
  registerTag(config: TagConfig): void {
    this.tags.set(config.name, config)
    console.log(`[TagManager] 标签已注册: ${config.name} (${config.address})`)
  }

  getTagConfig(name: string): TagConfig | undefined {
    return this.tags.get(name)
  }

  getAllTags(): TagConfig[] {
    return Array.from(this.tags.values())
  }

  // ========== 实时数据更新与报警评估 ==========
  updateValue(tagName: string, value: any): AlarmRecord | null {
    const tag = this.tags.get(tagName)
    if (!tag) return null

    this.values.set(tagName, value)

    // 评估报警规则
    let alarmRecord: AlarmRecord | null = null
    if (tag.alarmHighHigh && value >= tag.alarmHighHigh) {
      alarmRecord = this.createAlarm(tag, 'highHigh', value, tag.alarmHighHigh)
    } else if (tag.alarmHigh && value >= tag.alarmHigh) {
      alarmRecord = this.createAlarm(tag, 'high', value, tag.alarmHigh)
    }

    if (alarmRecord) {
      this.alarms.push(alarmRecord)
      this.emit('alarm', alarmRecord)
    }

    return alarmRecord
  }

  getValue(tagName: string): any {
    return this.values.get(tagName) ?? null
  }

  getAllValues(): Record<string, any> {
    const result: Record<string, any> = {}
    this.values.forEach((v, k) => { result[k] = v })
    return result
  }

  // ========== 报警处理 ==========
  private createAlarm(tag: TagConfig, type: string, value: number, threshold: number): AlarmRecord {
    return {
      id: `ALM-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
      tagName: tag.name,
      alarmType: type,
      alarmLevel: type === 'highHigh' ? '紧急' : '警告',
      currentValue: value,
      threshold,
      message: `${tag.description}: ${value} (阈值${threshold})`,
      timestamp: new Date(),
      acknowledged: false,
    }
  }

  acknowledgeAlarm(alarmId: string, userId: string): boolean {
    const alarm = this.alarms.find(a => a.id === alarmId)
    if (!alarm) return false
    alarm.acknowledged = true
    this.emit('alarm-acknowledged', { alarmId, userId })
    return true
  }

  getAlarms(filter?: { acknowledged?: boolean; level?: string; startDate?: Date; endDate?: Date }): AlarmRecord[] {
    let result = [...this.alarms]
    if (filter?.acknowledged !== undefined) {
      result = result.filter(a => a.acknowledged === filter.acknowledged)
    }
    if (filter?.level) {
      result = result.filter(a => a.alarmLevel === filter.level)
    }
    if (filter?.startDate) {
      result = result.filter(a => a.timestamp >= filter.startDate!)
    }
    if (filter?.endDate) {
      result = result.filter(a => a.timestamp <= filter.endDate!)
    }
    return result.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())
  }
}

WebSocket服务端推送:

// packages/backend/src/modules/websocket/ws.gateway.ts
import { Server as SocketIOServer } from 'socket.io'
import { createServer } from 'http'
import { TagManager } from '../tag/tag.manager'
import { ModbusTCPAdapter } from '../device/protocols/modbus-tcp.adapter'

export function createHMIWebSocketServer(httpServer: any) {
  const io = new SocketIOServer(httpServer, {
    cors: { origin: '*', methods: ['GET', 'POST'] },
    pingInterval: 10000,
    pingTimeout: 5000,
  })

  const tagManager = new TagManager()

  // 注册温度标签
  tagManager.registerTag({ name: 'barrel_temp', description: '料筒温度', deviceId: 'injection_molding', address: '300', dataType: 'float32', registerType: 'holding', scanRate: 500, alarmHigh: 280, alarmHighHigh: 320 })
  tagManager.registerTag({ name: 'nozzle_temp', description: '喷嘴温度', deviceId: 'injection_molding', address: '302', dataType: 'float32', registerType: 'holding', scanRate: 500, alarmHigh: 260, alarmHighHigh: 300 })
  tagManager.registerTag({ name: 'mold_temp',   description: '模具温度', deviceId: 'injection_molding', address: '304', dataType: 'float32', registerType: 'holding', scanRate: 500, alarmHigh: 120, alarmHighHigh: 160 })

  // 监听报警
  tagManager.on('alarm', (alarm) => {
    console.log(`[告警] ${alarm.message}`)
    io.emit('alarm:new', alarm)
  })

  io.on('connection', (socket) => {
    console.log(`[WebSocket] 客户端已连接: ${socket.id}`)

    // 订阅实时数据(客户端按需订阅具体标签)
    socket.on('subscribe:tags', (tagNames: string[]) => {
      const room = `tags:${tagNames.sort().join(',')}`
      socket.join(room)
    })

    // 报警确认
    socket.on('alarm:acknowledge', (data: { alarmId: string; userId: string }) => {
      const success = tagManager.acknowledgeAlarm(data.alarmId, data.userId)
      if (success) {
        io.emit('alarm:updated', { alarmId: data.alarmId, acknowledged: true })
      }
    })

    socket.on('disconnect', () => {
      console.log(`[WebSocket] 客户端已断开: ${socket.id}`)
    })
  })

  // Modbus模拟数据轮询(实际对接真实PLC)
  setInterval(async () => {
    const simulatedData = {
      barrel_temp: 250 + Math.random() * 60,
      nozzle_temp: 240 + Math.random() * 50,
      mold_temp: 90 + Math.random() * 40,
    }
    for (const [name, value] of Object.entries(simulatedData)) {
      tagManager.updateValue(name, Math.round(value * 10) / 10)
    }
    io.emit('tags:update', tagManager.getAllValues())
  }, 500)

  console.log('[WebSocket] HMI WebSocket服务已启动')
  return { io, tagManager }
}

6.3 前端实现

WebSocket通信层封装:

// packages/frontend/src/composables/useWebSocket.ts
import { io, Socket } from 'socket.io-client'
import { ref, onMounted, onUnmounted } from 'vue'
import type { AlarmRecord } from '@shared/types'

export function useHMIWebSocket(serverUrl: string = 'http://localhost:3000') {
  const socket = ref<Socket | null>(null)
  const connected = ref(false)
  const tagValues = ref<Record<string, number>>({})
  const alarms = ref<AlarmRecord[]>([])
  const unacknowledgedCount = ref(0)

  let reconnectTimer: ReturnType<typeof setTimeout> | null = null

  function connect() {
    socket.value = io(serverUrl, {
      transports: ['websocket'],
      reconnection: true,
      reconnectionDelay: 1000,
      reconnectionAttempts: 10,
    })

    socket.value.on('connect', () => {
      connected.value = true
      console.log('[HMI] WebSocket已连接')
    })

    socket.value.on('tags:update', (data: Record<string, number>) => {
      tagValues.value = { ...data }
    })

    socket.value.on('alarm:new', (alarm: AlarmRecord) => {
      alarms.value.unshift(alarm)
      unacknowledgedCount.value = alarms.value.filter(a => !a.acknowledged).length
    })

    socket.value.on('alarm:updated', (data: { alarmId: string; acknowledged: boolean }) => {
      const alarm = alarms.value.find(a => a.id === data.alarmId)
      if (alarm) alarm.acknowledged = data.acknowledged
      unacknowledgedCount.value = alarms.value.filter(a => !a.acknowledged).length
    })

    socket.value.on('disconnect', () => { connected.value = false })
  }

  function subscribeTags(tagNames: string[]) {
    socket.value?.emit('subscribe:tags', tagNames)
  }

  function acknowledgeAlarm(alarmId: string, userId: string) {
    socket.value?.emit('alarm:acknowledge', { alarmId, userId })
  }

  onMounted(() => { connect() })
  onUnmounted(() => {
    if (reconnectTimer) clearTimeout(reconnectTimer)
    socket.value?.disconnect()
  })

  return { connected, tagValues, alarms, unacknowledgedCount, subscribeTags, acknowledgeAlarm }
}

Pinia Store:

// packages/frontend/src/stores/hmi.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { useHMIWebSocket } from '@/composables/useWebSocket'

export const useHMIStore = defineStore('hmi', () => {
  const { connected, tagValues, alarms, unacknowledgedCount, subscribeTags, acknowledgeAlarm } = useHMIWebSocket()

  // 订阅需要的标签
  subscribeTags(['barrel_temp', 'nozzle_temp', 'mold_temp'])

  // 各温度点的计算属性
  const barrelTemp = computed(() => tagValues.value.barrel_temp ?? '--')
  const nozzleTemp = computed(() => tagValues.value.nozzle_temp ?? '--')
  const moldTemp   = computed(() => tagValues.value.mold_temp   ?? '--')

  // 温度状态判断
  function getTempStatus(temp: number, high: number, highHigh: number): 'normal' | 'warning' | 'critical' {
    if (temp >= highHigh) return 'critical'
    if (temp >= high) return 'warning'
    return 'normal'
  }

  const barrelStatus = computed(() => getTempStatus(Number(barrelTemp.value), 280, 320))
  const nozzleStatus = computed(() => getTempStatus(Number(nozzleTemp.value), 260, 300))
  const moldStatus   = computed(() => getTempStatus(Number(moldTemp.value), 120, 160))

  return {
    connected, barrelTemp, nozzleTemp, moldTemp,
    barrelStatus, nozzleStatus, moldStatus,
    alarms, unacknowledgedCount, acknowledgeAlarm,
  }
})

温度监控画面核心组件示例:

<!-- packages/frontend/src/views/TemperatureMonitor.vue -->
<template>
  <div class="temperature-monitor">
    <!-- 顶部状态栏 -->
    <div class="status-bar">
      <span class="connection-status" :class="{ connected: store.connected }">
        {{ store.connected ? '● 已连接' : '○ 未连接' }}
      </span>
      <el-badge :value="store.unacknowledgedCount" :hidden="store.unacknowledgedCount === 0">
        <el-button type="danger" size="small" @click="showAlarmPanel = true">
          🚨 报警
        </el-button>
      </el-badge>
    </div>

    <!-- 温度仪表盘 -->
    <div class="temp-panel">
      <TempGauge
        title="料筒温度"
        :value="store.barrelTemp"
        unit="°C"
        :min="0" :max="400"
        :warning="280" :critical="320"
        :status="store.barrelStatus"
      />
      <TempGauge
        title="喷嘴温度"
        :value="store.nozzleTemp"
        unit="°C"
        :min="0" :max="400"
        :warning="260" :critical="300"
        :status="store.nozzleStatus"
      />
      <TempGauge
        title="模具温度"
        :value="store.moldTemp"
        unit="°C"
        :min="0" :max="200"
        :warning="120" :critical="160"
        :status="store.moldStatus"
      />
    </div>

    <!-- 报警弹窗 -->
    <el-dialog v-model="showAlarmPanel" title="实时报警" width="700px">
      <el-table :data="store.alarms" max-height="400" stripe>
        <el-table-column prop="timestamp" label="时间" width="170" />
        <el-table-column prop="tagName" label="标签" width="100" />
        <el-table-column prop="message" label="报警信息" />
        <el-table-column prop="alarmLevel" label="级别" width="70">
          <template #default="{ row }">
            <el-tag :type="row.alarmLevel === '紧急' ? 'danger' : 'warning'" size="small">
              {{ row.alarmLevel }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="状态" width="80">
          <template #default="{ row }">
            <span :style="{ color: row.acknowledged ? '#67c23a' : '#f56c6c' }">
              {{ row.acknowledged ? '已确认' : '未确认' }}
            </span>
          </template>
        </el-table-column>
        <el-table-column label="操作" width="80">
          <template #default="{ row }">
            <el-button v-if="!row.acknowledged" type="primary" size="small" @click="store.acknowledgeAlarm(row.id, 'operator')">确认</el-button>
          </template>
        </el-table-column>
      </el-table>
    </el-dialog>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { useHMIStore } from '@/stores/hmi'
import TempGauge from '@/components/hmi/TempGauge.vue'

const store = useHMIStore()
const showAlarmPanel = ref(false)
</script>

<style scoped>
.temperature-monitor {
  height: 100vh;
  background: #0a1628;
  color: #e0e0e0;
}
.status-bar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 12px 24px;
  background: rgba(255, 255, 255, 0.05);
}
.connection-status { font-size: 14px; color: #f56c6c; }
.connection-status.connected { color: #67c23a; }
.temp-panel {
  display: flex;
  justify-content: center;
  gap: 40px;
  padding: 60px 20px;
  flex-wrap: wrap;
}
</style>

七、与竞品分析对比及脱颖而出策略

7.1 竞品矩阵分析

当前工业Web HMI市场存在以下几类竞争方案:

7.1.1 商业HMI/SCADA平台
竞品 定位 技术栈 核心优势 核心劣势 典型价格
西门子WinCC Unified 高端全集成 HTML5/SVG/JS (Web) 全集成自动化、TIA Portal一体化、工业级可靠性 封闭生态、授权费用高、需要西门子PLC配合 数万-数十万
力控FWebSCADA 国产SCADA云平台 Web组态+SmartDB 采集/存储/服务一体化、支持OPC UA/Modbus、国产信创 商业授权、定制化受限 按点收费
N3uron Web Vision 纯Web HMI/SCADA JS/HTML5/CSS 标签/连接/设备数无限制、跨全设备类型、纯Web无插件 按模块收费、中小企业门槛高 按模块订阅
HELIO Web HMI开发平台 React+Rust+WASM 强大TypeScript SDK、响应式UI、OPC UA/MQTT原生支持 闭源商业产品、React生态绑定 商业授权

与商业平台对比,自研方案的核心优势:

  • 零授权费用: 商业HMI通常按点数或模块收费,1000个标签点的授权费可达数万至数十万元
  • 完全定制化: 商业平台的UI风格、交互逻辑、功能边界受限于厂商设计,自研方案可根据行业需求深度定制
  • 集成灵活性: 自研方案可无缝对接企业现有IT系统,不受厂商SDK和API边界的限制
7.1.2 开源HMI/SCADA框架
竞品 定位 技术栈 核心优势 核心劣势
FUXA 开源Web SCADA/HMI Vue.js + Node.js 低代码可视化配置、多协议支持(Modbus/OPC UA/MQTT)、跨平台部署、社区活跃 核心开发者主导、定制度受限于框架架构、界面风格偏传统
ISM Web组态 开源IoT组态系统 Vue.js + Go 1000+工业组件、多协议支持(Modbus/OPC UA/S7)、终身免费 Go后端增加技术栈复杂度、社区规模较小
WebTopo 开源Web组态编辑器 Vue.js + Quasar SVG自由缩放、拖拽设计、组件系统融合DOM/Canvas/ECharts 侧重编辑器本身、缺乏完整HMI后端

FUXA基于Web的HMI/SCADA开源方案,其交互逻辑和扩展方式受限于框架架构。FUXA的功能边界由核心团队决定,二次开发需要深入理解其内部代码结构和设计模式。

7.1.3 无代码WebHMI平台

无代码平台支持跨平台访问,通过拖拽式操作几分钟内即可完成界面设计,无需购买昂贵的硬件设备。但其通用化设计难以适配特定行业的深度需求(如制药行业的电子签名和审计追踪、电力行业的IEC 61850协议),复杂交互逻辑(如多设备联动控制、条件分支操作)在无代码平台中实现困难,高度依赖特定云平台存在供应商锁定风险。

7.1.4 传统嵌入式HMI

传统HMI的根本缺陷在于封闭的架构——依赖专用硬件终端、需要安装特定运行环境和驱动、缺乏原生远程访问能力、功能升级或Bug修复必须派遣工程师到客户现场手动操作、设备数据成为“孤岛”。

7.2 Vue3+Node.js自研方案的差异化优势总览

维度 Vue3+Node.js自研 商业HMI平台 开源框架 无代码平台 传统嵌入式HMI
授权费用 ✅ 零授权费 ❌ 数万-数十万 ✅ 免费 ⚠️ 按量收费 ❌ 硬件绑定
代码掌控 ✅ 100%自主 ❌ 黑盒 ⚠️ 需深入理解 ❌ 闭源 ❌ 闭源
定制深度 ✅ 无限制 ⚠️ 受限于SDK ⚠️ 受限于架构 ❌ 通用化设计 ❌ 厂商锁定
跨平台 ✅ 全端覆盖 ✅ Web ✅ Web ✅ Web ❌ 单设备
远程运维 ✅ 原生支持 ❌ 需额外方案
IT/OT融合 ✅ API自由设计 ⚠️ 有限接口 ⚠️ 有限 ⚠️ 受限于平台 ❌ 封闭
AI集成 ✅ 自由接入 ⚠️ 受限 ❌ 不支持 ❌ 不支持 ❌ 不支持
技术门槛 ⚠️ 需全栈团队 ✅ 低(组态) ⚠️ 中等 ✅ 极低 ✅ 低
维护成本 ✅ 自主可控 ⚠️ 依赖厂商 ⚠️ 依赖社区 ⚠️ 依赖平台 ✅ 低

7.3 如何脱颖而出:六大差异化策略

策略一:AI驱动的智能运维集成

在所有竞品方案中,AI原生集成是自研方案最独特的差异化优势。可直接在后端集成TensorFlow.js或ONNX Runtime,实现:

  • 预测性报警: 分析历史趋势数据,在温度达到阈值之前就预测异常趋势并提前预警
  • 智能操作建议: 根据当前设备状态、历史数据和最佳实践模型,自动给出参数调整建议
  • 自然语言查询: 操作员可直接输入“上周三料筒温度最高多少度”进行查询
策略二:深度行业化模板与快速交付

针对特定垂直行业(注塑、包装、暖通、水处理等),预置完整的行业画面模板和报警规则库,实现半天内完成从部署到上线的交付速度。

策略三:原生级数字孪生3D可视化

利用Three.js或Babylon.js实现设备3D模型的Web端实时渲染,模型颜色、动画、数据标注与真实设备状态实时同步。3D视角直观展示设备内部结构,快速定位故障点;在虚拟环境中模拟操作指令效果后再下发,降低操作风险。

策略四:极致开放的多协议互联

自研方案的协议适配器支持热插拔式扩展,每个新协议驱动只需实现标准接口即可接入系统。随着工业互联网对统一数据互通标准的要求提升,OPC UA等开放协议的支持愈发重要。

策略五:数据驱动的增值服务
  • 自动核算设备综合效率(OEE)并以图表直观呈现
  • 可配置报表模板并定时邮件推送
  • 设备健康度评分与主动维保建议
策略六:开发者生态与社区运营

开放组态编辑器的组件SDK,允许第三方开发者贡献图元组件和协议驱动。建立组件市场和模板市场,形成开发者社区生态。提供完善的REST API和Webhook机制,便于系统集成商二次开发。

八、总结与建议

8.1 方案适用场景

Vue3 + Node.js全栈自研Web HMI方案最适用于:

  • 设备制造商: 需要为自有设备配套差异化HMI产品,建立品牌辨识度
  • 系统集成商: 项目需求多变,需要灵活定制,且看重长期成本优势
  • 中大型制造企业: 有自建IT团队,需要HMI与MES/ERP深度融合
  • 有AI/数据分析需求的企业: 需要将预测性维护、OEE分析等能力原生集成到HMI中

8.2 资源投入估算

资源 最小团队 推荐团队
前端工程师(Vue3) 1人 2人
后端工程师(Node.js) 1人 2人
工业自动化工程师 1人(兼职) 1人
UI/UX设计师 0.5人(兼职) 1人
QA测试 0.5人(兼职) 1人

8.3 风险与应对

风险 影响 应对策略
工业协议兼容性不足 优先实现Modbus和OPC UA;非标协议通过脚本引擎扩展
实时性能瓶颈 WebSocket + 数据分片;大规模场景引入Redis Stream
团队技术门槛 先用FUXA快速验证,再逐步替换核心模块
长期维护成本 Monorepo统一管理、自动化CI/CD、完善文档

8.4 最终建议

如果团队具备Vue3和Node.js的全栈能力,建议采用“核心自研 + 生态借力”的渐进策略——关键差异化模块(组态编辑器、报警引擎、AI集成、行业模板)完全自主研发以构建核心壁垒;通用基础能力(协议驱动、数据存储、认证授权)充分利用成熟开源库加速开发;在项目初期可参考FUXA或ISM等开源方案的架构设计思路,但保持代码层面的独立性和可控性。

Web HMI的核心价值在于将传统封闭的工业可视化系统转变为开放、灵活、可定制的平台。Vue3 + Node.js全栈技术方案为这一转变提供了坚实的技术基础——在保证工业级可靠性的前提下,赋予开发者前所未有的自由度与创新能力。

更多推荐