Vue3 储能 EMS 前端实战(下):Element Plus 深度定制与工程总结

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

适用读者:Vue3 开发者、Element Plus 二次定制、大型 B 端项目架构
技术栈:Vue 3.5 · Element Plus · SCSS · UnoCSS · ECharts · Three.js
项目类型:储能云平台 EMS UI


一、EMS 前端的 UI 体系挑战

配置化组件(HForm / HTable)和实时组态(MQTT + Maotu)解决的是功能和数据问题。但 EMS 项目还有第三个挑战:

  • 上百页面需要统一的视觉语言
  • Tab 导航 + 表格撑满剩余高度,Layout 链路必须闭环
  • Element Plus 默认样式与设计稿差距大,需要系统性覆盖

本篇聚焦 HTabs 自定义指示线全局 EP 主题定制,以及全项目的工程经验汇总。


二、整体架构回顾

样式层

视图层

业务页面 100+

HForm 配置化表单

HTable 配置化表格

HPreview 组态预览

HTabs 导航

styles/element/index.scss

UnoCSS 原子类

h-tabs-page-content

工程化配套

  • Vite 8 + unplugin-auto-importrefcomputed 等自动导入
  • vue-i18n$t('1001142') 形式的多语言 key
  • UnoCSS + SCSS:原子化 + 主题变量双层样式体系
  • postcss-pxtorem + amfe-flexible:大屏适配

三、HTabs 自定义指示线 —— 突破 Element Plus 限制

Element Plus Tabs 的 active-bar 是内置动画条,设计稿要求下划线指示器且宽度跟随 Tab 文字。

3.1 方案:CSS 变量 + JS 同步

隐藏 EP 默认 active-bar,用 CSS 伪元素 + 变量驱动指示线位置:

// styles/element/index.scss
@mixin h-tabs-line-nav {
  &::after {
    content: '';
    position: absolute;
    bottom: 0;
    left: var(--h-tabs-bar-x, 0);
    width: var(--h-tabs-bar-width, 0);
    height: 2px;
    background: #2897ff;
    transition: left 0.2s, width 0.2s;
  }
}

.h-tabs .ep-tabs__active-bar {
  display: none;
}
// HTabs/index.vue
const applyIndicator = (item: HTMLElement) => {
  navEl.style.setProperty('--h-tabs-bar-x', `${item.offsetLeft}px`)
  navEl.style.setProperty('--h-tabs-bar-width', `${item.offsetWidth}px`)
  navEl.classList.add('has-indicator')
}

3.2 监听 DOM 变化

Tab 动态增删、权限过滤导致 Tab 数量变化、国际化切换文字宽度变化时,指示线会错位。用 MutationObserver + ResizeObserver + requestAnimationFrame 三重保障:

const scheduleSync = () => {
  cancelAnimationFrame(rafId)
  rafId = requestAnimationFrame(syncActiveIndicator)
}

const observer = new MutationObserver(() => scheduleSync())
observer.observe(navEl, { attributes: true, childList: true, subtree: true })

const resizeObserver = new ResizeObserver(() => {
  clearTimeout(resizeTimer)
  resizeTimer = setTimeout(scheduleSync, 80)
})

踩坑:同步读取 offsetWidth 会导致指示线闪烁,必须用 requestAnimationFrame 延迟同步。

3.3 页面模式:Tab 导航 + keep-alive 内容区

EMS 大量页面采用「Tab 切换子模块」模式:

<h-tabs v-model="activeTab">
  <el-tab-pane v-for="tab in tabs" :label="tab.label" :name="tab.name" />
</h-tabs>
<div class="contentBox h-tabs-page-content">
  <keep-alive>
    <component :is="activeComponent" />
  </keep-alive>
</div>

配合权限过滤:

const tabs = allTabs.filter(tab =>
  userInfo.permissions.includes('*:*:*') ||
  userInfo.permissions.includes(tab.permit)
)

样式上 h-tabs-page-content 撑满剩余高度:

.h-tabs-page-content {
  flex: 1;
  display: flex;
  flex-direction: column;
  min-height: 0;
  height: calc(100% - var(--h-tabs-header-height, 48px));
  background-color: #fff;
}

这与 HTable 的 ResizeObserver 形成完整布局链:Layout flex → Tab 内容区 → 表格自适应高度

源码位置src/components/HTabs/index.vuesrc/styles/element/index.scss


四、Element Plus 全局主题定制要点

项目在 src/styles/element/index.scss 中系统性覆盖 EP 组件,核心策略:

策略 示例
CSS 变量覆盖 --ep-table-border-color--ep-checkbox-checked-bg-color
:deep() 穿透 HTableClean 表格边框、HForm 查看态样式
Mixin 复用 h-tabs-line-navgenerate-custom-link-colors
场景化类名 .h-tabs--segment.scene-tab.look-form

Dialog 统一风格

.ep-dialog {
  padding: 0;

  .ep-dialog__header {
    background: #f2f4f7;
    height: 42px;
    line-height: 42px;
  }

  .ep-dialog__body {
    padding: 22px;
  }
}

自定义 Loading(v-loadingh)

.ep-loading-mask {
  background-color: rgba(252, 252, 252, 0.15) !important;
}

.ep-loading-spinner .path {
  stroke: #0279e8 !important;
}

HLink 业务色扩展:通过 SCSS mixin 生成 b1~b4addlookwarn 等业务按钮色。


五、其他特色模块速览

模块 特色 典型场景
Echart 封装 统一主题、resize 监听 电量曲线、收益分析
GlbViewer Three.js 加载 3D 模型 储能设备 3D 展示
HAmap / 天地图 站点地理分布 运营大屏
Decimal.js 避免 JS 浮点误差 电量、金额计算
exceljs + html2canvas 报表导出 PDF / Excel 下载
v-loadingh 品牌色 Loading 全局加载态

六、全系列工程经验总结

✅ 值得借鉴

  1. 配置驱动 + Slot 扩展:80% 页面用 HForm / HTable 配置,20% 特殊场景用插槽
  2. Composable 抽离useHTableuseDictsuseLoad 让组件保持轻薄
  3. 全局类型声明HFormItemHColumncustom-declare.d.ts,IDE 自动补全体验好
  4. MQTT 生命周期闭环:connect → 申请权限 → subscribe → unsubscribe → disconnect
  5. 样式分层:UnoCSS 原子类 + SCSS 主题变量 + :deep() 覆盖 EP 组件
  6. Layout 链路:每层 flex:1; min-height:0,表格才能正确撑满

⚠️ 踩坑记录(全系列汇总)

问题 原因 解法 涉及篇目
固定列边框消失 EP 滚动状态类名层级不对 选择器写到 .ep-table.is-scrolling-xxx 第 1 篇
表格高度为 0 父容器无明确高度 Layout 链路上每层 flex:1; min-height:0 第 1、3 篇
MQTT 通配符不生效 mqtt.js 不支持客户端 + 匹配 自写正则路由 第 2 篇
Maotu 条件渲染后空白 挂载前调 API nextTick 后再 setImportJson 第 2 篇
Tab 指示线闪烁 同步读 offsetWidth requestAnimationFrame 延迟同步 第 3 篇
切换站点数据错乱 旧 topic 未 unsubscribe 生命周期内显式清理订阅 第 2 篇

七、系列结语

这个 EMS 前端项目的核心价值,不在于用了多少库,而在于针对工业场景做了正确的抽象

挑战 解法 系列篇目
上百 CRUD 页面 HForm / HTable 配置化 第 1 篇
实时监控 MQTT + 组态联动 第 2 篇
统一 UI 规范 Element Plus 深度主题定制 第 3 篇

三篇文章构成完整链路:效率(配置化)→ 实时(MQTT + 组态)→ 体验(UI 体系)

如果你也在做 IoT / 能源 / 工业互联网前端,希望本系列能帮你少踩几个坑。


系列目录

更多推荐