Vue3 储能 EMS 前端实战(上):配置化表单与表格体系

系列说明:本文是《Vue3 储能 EMS 前端实战》第 1 篇 / 共 3 篇

适用读者:Vue3 中高级开发者、工业物联网 / 能源管理系统前端工程师
技术栈:Vue 3.5 · Vite 8 · Element Plus · Pinia · Decimal.js
项目类型:储能云平台 EMS UI


一、为什么 EMS 前端需要配置化?

储能能源管理系统(EMS)前端和普通后台管理系统有本质区别:

维度 普通 Admin EMS 前端
数据特征 请求-响应,静态为主 秒级实时推送
页面形态 表格 + 表单 组态拓扑图 + 实时曲线 + 3D 模型
页面数量 几十页 上百页,高度重复

上百个 CRUD 页面如果每个都手写 <el-form> + <el-table>,维护成本会爆炸。本项目的第一层抽象是:用 JSON 配置描述 UI,用 Slot 处理 20% 的特殊场景。

HFormItem 数组

HForm 组件

HColumn 数组

HTable 组件

业务页面

Slot 扩展特殊字段

本篇聚焦HForm 表单引擎 + HTable 表格引擎 + useHTable Composable。


二、配置化表单 HForm —— 用 JSON 描述 UI

2.1 设计思路

传统写法:每个页面手写 <el-form> + 十几个 <el-form-item>,字段一多就失控。

本项目抽象出全局类型 HFormItem(定义在 custom-declare.d.ts),用数组配置描述整个表单:

// 典型用法:查询表单
const queryFormItems = shallowRef<HFormItem[]>([
  { label: '设备类型', type: 'select', prop: 'deviceType', options: deviceTypes, span: 6 },
  { label: '设备名称', type: 'input',  prop: 'deviceName', span: 6 },
  { label: '时间范围', type: 'daterange', prop: 'dateRange', span: 6 },
])

模板侧只需一行:

<h-form :form-items="queryFormItems" :model="queryFormData" isquery @search="handleSearch" />

2.2 核心实现:layoutItems 自动换行算法

HForm 不是简单 v-for,而是内置栅格换行布局引擎

// 核心逻辑(简化版)
const layoutItems = computed(() => {
  const result = []
  let i = 0
  while (i < items.length) {
    const rowItems = []
    let rowSpan = 0
    // 按 span 累加,满 24 换行;span=24 独占一行
    while (i < items.length) {
      const span = item.span || (isquery ? 6 : 12)
      if (span === 24) { /* 独占一行 */ break }
      if (rowSpan + span > 24) break
      rowItems.push(item)
      rowSpan += span
      i++
      if (rowSpan === 24) break
    }
    // 为每列计算对齐类:set-left / set-center / set-right
    rowItems.forEach((item, index) => {
      result.push({ item, span, alignClass: getAlignClass(index, spans, rowSpan) })
    })
  }
  return result
})

三种模式

模式 触发条件 行为
isquery 查询表单 默认 span=6,前 3 项 + 搜索按钮,超出可展开
编辑表单 默认 span=12,左/中/右对齐,固定 340px 输入宽度
disabled 查看详情 禁用态样式,空值隐藏 placeholder

2.3 校验体系:函数式 Rule + Decimal.js

数字字段支持 min / max / step 校验,步长用 Decimal.js 避免浮点误差:

// step 校验:value 必须是 step 的整数倍
rules.push(createRule((value) =>
  new Decimal(value).mod(item.attrs!.step!).toNumber() !== 0
    ? `请输入 ${item.attrs!.step} 的倍数`
    : undefined
))

自定义校验支持函数数组,比 Element Plus 原生 rules 更简洁:

rules: [
  (val) => val < 0 ? '不能为负数' : undefined,
  (val) => val > 100 ? '不能超过100' : undefined,
]

2.4 扩展点:Slot 插槽

配置覆盖不了的场景,用 #prop名 插槽:

<h-form :form-items="formItems" :model="formData">
  <template #customField>
    <my-special-component v-model="formData.customField" />
  </template>
</h-form>

源码位置src/components/HForm/index.vuecustom-declare.d.ts


三、配置化表格 HTable —— 自适应高度 + 列显隐

3.1 useHTable Composable 设计

表格逻辑从组件中抽离到 useHTable.ts,实现逻辑复用HTableCleanHTableBorder 共用同一套逻辑)。

亮点 1:ResizeObserver 自动计算表格高度

EMS 页面常见布局:顶部 Tab + 查询表单 + 表格撑满剩余空间。手动算高度很痛苦,项目用 ResizeObserver 监听父容器:

const getTableHeight = () => {
  // 遍历兄弟节点,累加高度
  for (let el of siblingNodes) {
    if (el !== tableContainer.value) {
      sum += el.getBoundingClientRect().height + marginTop + marginBottom
    }
  }
  // 父容器高度 - padding - 兄弟高度 = 表格可用高度
  height.value = Math.max(parentHeight - sum, minHeight ?? 280)
}

亮点 2:列宽智能计算

支持 "10z" 这种写法,表示 10 个字符宽度,内部用 Canvas measureText 精确测量:

if (/\d+[zZ]{1}/.test(col.width)) {
  col.width = calculateTextWidth("Z".repeat(parseInt(col.width))) + 24
}

亮点 3:列显隐设置

setting=true 时弹出 Popover,用户勾选可见列,存入 selectedColumns,持久化可扩展 localStorage。

3.2 HTableClean:Element Plus 表格边框的深度覆盖

Element Plus 默认 border 表格四边都有线,设计稿要求:

  • 去掉单元格右边框
  • 表头用伪元素做列间分隔线(高度 26px,垂直居中)
  • 固定列在所有滚动状态下都显示分隔线
.htable-clean {
  :deep(td.ep-table__cell) {
    border-right: none !important;
    border-bottom: 1px solid var(--ep-table-border-color) !important;
  }

  :deep(th.ep-table__cell) {
    border-right: none !important;
    position: relative;

    &::after {
      content: " ";
      width: 1px;
      height: 26px;
      position: absolute;
      top: 50%;
      right: 0;
      transform: translateY(-50%);
      background-color: var(--ep-table-border-color);
    }
  }

  // 关键:覆盖 EP 在不同 is-scrolling-xxx 状态下的固定列边框
  :deep(.ep-table.is-scrolling-none),
  :deep(.ep-table.is-scrolling-left),
  :deep(.ep-table.is-scrolling-right),
  :deep(.ep-table.is-scrolling-middle) {
    td.ep-table-fixed-column--left.is-last-column,
    th.ep-table-fixed-column--left.is-last-column {
      border-right: 1px solid var(--ep-table-border-color) !important;
    }
  }
}

踩坑is-scrolling-xxx 类名在 .ep-table 上,不在 .htable-clean 上,选择器层级要写对。

源码位置src/components/HTable/useHTable.tssrc/components/HTable/HTableClean.vue


四、本篇小结

组件 解决的问题 关键技术
HForm 上百页表单重复劳动 栅格换行算法、函数式校验、Slot 扩展
HTable 表格高度自适应、列配置 ResizeObserver、Canvas 测宽、Composable 复用
HTableClean 设计稿与 EP 默认样式差异 :deep() 覆盖、伪元素分隔线

配置化解决了 EMS 项目 80% 页面的开发效率问题。下一篇进入工业场景的核心难点:MQTT 实时推送 + Maotu 组态联动

👉 下一篇Vue3 储能 EMS 前端实战(中):MQTT 实时数据与组态联动

更多推荐