11 工业级 UI 开发
工业级 UI 开发
关联:索引
问题探究:
-
为什么工业后台要用组件库,能解决哪些“自己写 UI 写不完/写不稳/写不一致”的工程问题
-
Element Plus 的“全局引入/按需引入/自动导入”差别是什么,为什么一不小心就样式丢失或体积膨胀
-
国际化到底分几层:Element Plus 组件语言、业务文案语言、日期/数字格式化怎么统一
-
表格/表单/弹窗/分页的“联动闭环”怎么做,才能做到筛选不乱、翻页不丢条件、操作可追溯
-
工业智能系统界面风格有哪些硬规则(信息密度、状态色、留白、对齐、可读性),怎么落到代码里
-
仪表盘、设备列表、数据筛选、响应式布局的常见坑是什么(高度撑不满、表格抖动、弹窗表单残留、移动端折叠)
-
health表示设备健康:ok正常、warn预警、alarm告警、offline离线 -
alarmLevel表示告警等级:none/L1/L2/L3(从低到高)
| 字段 | 含义 | 示例 | UI 展示建议 |
|---|---|---|---|
| deviceId | 设备编号 | SORT-01 |
可复制,作为主键 |
| deviceName | 设备名称 | 一号分拣机 |
表格主列 |
| line | 产线/区域 | A 线 |
用于筛选与分组 |
| station | 工位/站点 | 入料口-1 |
次级信息 |
| health | 健康状态 | ok/warn/alarm/offline |
用 Tag 显示(颜色统一) |
| alarmLevel | 告警等级 | none/L1/L2/L3 |
与 health 联动显示 |
3) TypeScript 类型建议(便于 AI 生成一致代码)
export type Health = 'ok' | 'warn' | 'alarm' | 'offline'
export type AlarmLevel = 'none' | 'L1' | 'L2' | 'L3'
export interface DeviceRow {
deviceId: string
deviceName: string
line: string
station: string
health: Health
alarmLevel: AlarmLevel
heartbeatAt: number
throughputPerMin: number
rejectRate: string
lastErrorCode?: string
updatedAt: string
}
export interface DeviceQueryForm {
deviceName: string
health: Health | ''
alarmLevel: AlarmLevel | ''
line: string
station: string
}
- “启用/停用”属于高风险操作:必须二次确认,并提示影响(会影响该设备分拣任务)
- “告警确认”不等于“告警消除”:确认后状态可从
alarm降为warn,但不应直接变ok(除非设备心跳与指标恢复) - “离线”优先级最高:离线时不允许执行启停/参数下发等操作(按钮置灰并给原因)
作业:
布置
1)完成一个完整业务页面(仪表盘 / 设备列表二选一)。
2)实现响应式布局、表格、筛选、分页。
3)保证界面整洁、交互正常、符合工业系统风格。
1. 你必须记住的 3 个边界
-
组件库解决“通用 UI 与交互一致性”,不解决业务逻辑(业务规则仍要自己写)
-
风格规范要先定“信息层级与对齐规则”,再定颜色与圆角(先可读、再好看)
-
页面状态必须完整:加载中/空数据/错误/无权限(工业系统最怕“用户不知道发生了什么”)
-
信息密度:优先“可扫读”,减少大面积装饰;表格行高、列宽要稳定
-
状态色:仅用于状态(运行/停机/告警/离线),不要把所有按钮都染色
-
层级:标题 > 分区标题 > 辅助说明;用 Card/分割线建立区域
-
交互语义:主按钮=“确认/保存/执行”;危险按钮=“删除/停用”;次按钮=“取消/返回”
-
可追溯:操作要有确认(Confirm)或二次提示,关键字段要可复制/可查看详情
-
第 1 层:Element Plus 组件内置文案(Pagination、DatePicker、Empty)
-
第 2 层:业务文案(设备状态、告警级别、按钮文字)
-
第 3 层:日期/数字格式(可先不做,后续项目再统一)
前置条件
已具备:Vue3 + Vite + TypeScript 初始化项目。
安装依赖:
npm i element-plus @element-plus/icons-vue
步骤 1:在 main.ts 集成 Element Plus(含国际化)
示例(来自 09_element_plus_demo/src/main.ts,Element Plus 中文 + 图标 + Pinia/Router):
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
const app = createApp(App)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(ElementPlus, {
locale: zhCn,
})
app.use(createPinia())
app.use(router)
app.mount('#app')
- 页面按钮/表格是否有样式(若无,大概率是 CSS 未引入)
- Pagination 的“上一页/下一页”等是否为中文(若非中文,大概率 locale 未生效)
- 控制台是否无重复注册/导入错误
步骤 2:搭建后台页面骨架(Layout)
- 顶部:系统标题 + 当前模块 + 用户区(可后续接入登录态)
- 左侧:菜单(设备/任务/告警/设置)
- 主区:内容(Card 分块)
示例结构(来自 09_element_plus_demo/src/App.vue,包含菜单路由联动与图标):
<template>
<el-container style="height: 100vh; width: 100%">
<el-aside width="220px" style="background-color: #2f4050">
<div class="logo">智能分拣系统</div>
<el-menu
:default-active="activeMenu"
mode="vertical"
background-color="#2f4050"
text-color="#fff"
active-text-color="#ffd04b"
router
>
<el-menu-item index="/dashboard">
<el-icon><House /></el-icon>
<span>仪表盘</span>
</el-menu-item>
<el-menu-item index="/device-list">
<el-icon><Box /></el-icon>
<span>设备列表</span>
</el-menu-item>
</el-menu>
</el-aside>
<el-container>
<el-header style="text-align: right; font-size: 12px; background: #fff; border-bottom: 1px solid #e6e6e6; line-height: 60px; padding: 0 20px">
管理员
</el-header>
<el-main style="background: #f3f3f4; padding: 20px; overflow: auto">
<router-view />
</el-main>
</el-container>
</el-container>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { House, Box } from '@element-plus/icons-vue'
const route = useRoute()
const activeMenu = computed(() => route.path)
</script>
步骤 3:创建“设备列表”页面骨架(筛选 + 表格 + 分页)
- 查询区(Form inline)
- 表格区(Table)
- 分页区(Pagination)
- 弹窗区(Dialog:新增/编辑/详情)
页面结构建议(用于 AI 生成的需求描述):
- 顶部 Card:筛选表单(设备名、状态、位置、时间范围)
- 中部 Card:表格(编号/名称/状态/最后心跳/位置/操作)
- 底部:分页(page/pageSize/total)
常见错误排查(当堂必讲)
-
样式丢失:是否引入
element-plus/dist/index.css -
组件语言不生效:是否
app.use(ElementPlus, { locale: zhCn }) -
表格宽度抖动:是否列宽不固定、内容过长未处理(可用
show-overflow-tooltip) -
页面太“花”:是否滥用颜色/阴影(工业风格优先克制)
-
查询条件(filters)
-
分页状态(page/pageSize/total)
-
表格数据(rows)
-
弹窗表单(dialogVisible + formModel + formRules)
规则:条件变化 → 回到第一页;提交成功 → 关闭弹窗并刷新列表;关闭弹窗 → 重置表单与校验状态。
- 操作按钮不超过 3 个(更多用下拉菜单)
- 危险操作必须二次确认(确认文案要“明确后果”)
- 状态切换要可见反馈(启停后更新标签/行状态)
项目工坊主题:实现智能分拣系统仪表盘、设备列表、筛选、分页、操作栏。
A. 仪表盘(Dashboard)建议实现点
- 指标卡片(Card):在线设备数、告警数、今日分拣量、异常率
- 图表区域(Chart):可先用占位容器(第二阶段再接真实图表)
- 最近告警/任务(Table):展示最近 5 条,支持“查看详情”
示例代码(来自 09_element_plus_demo/src/views/DashboardView.vue,最近告警表格 + Tag 映射):
import type { AlarmLevel } from '../types/device'
const getAlarmTagType = (level: AlarmLevel) => {
const map = {
none: 'info',
L1: 'warning',
L2: 'danger',
L3: 'danger',
}
return map[level]
}
- 在线设备数:
health !== 'offline'的设备数量 - 告警数:
health === 'alarm'的设备数量(可加分:按 L1/L2/L3 分组)
最近告警表(字段建议):
- 时间(occurredAt)
- 设备(deviceId/deviceName)
- 等级(alarmLevel)
- 代码(errorCode)
- 描述(message)
- 操作(查看/确认)
B. 设备列表(Device List)必做闭环
示例代码(来自 09_element_plus_demo/src/views/DeviceListView.vue,建议学生对照复现并再抽取复用):
- 状态 Tag 映射(统一文案与颜色):
import type { Health, AlarmLevel } from '../types/device'
const getHealthTagType = (health: Health) => {
const map = { ok: 'success', warn: 'warning', alarm: 'danger', offline: 'info' }
return map[health]
}
const getHealthText = (health: Health) => {
const map = { ok: '正常', warn: '警告', alarm: '告警', offline: '离线' }
return map[health]
}
const getAlarmTagType = (level: AlarmLevel) => {
const map = { none: 'info', L1: 'warning', L2: 'danger', L3: 'danger' }
return map[level]
}
- 离线判定(状态与操作联动的前置口径):
import type { DeviceRow } from '../types/device'
const updateOfflineStatus = (list: DeviceRow[]): DeviceRow[] => {
const now = Date.now()
return list.map(item => ({
...item,
health: now - item.heartbeatAt > 180000 ? 'offline' : item.health,
}))
}
- 筛选 + 分页闭环(筛选/重置回到第 1 页;分页保留筛选条件):
import { ref, reactive } from 'vue'
import type { DeviceRow, DeviceQueryForm } from '../types/device'
const queryForm = reactive<DeviceQueryForm>({
deviceName: '',
health: '',
alarmLevel: '',
line: '',
station: '',
})
const pageInfo = reactive({ page: 1, pageSize: 10, total: 0 })
const originDeviceList = ref<DeviceRow[]>([])
const deviceList = ref<DeviceRow[]>([])
const filterDeviceList = () => {
let data = updateOfflineStatus([...originDeviceList.value])
if (queryForm.deviceName) data = data.filter(i => i.deviceName.includes(queryForm.deviceName))
if (queryForm.health) data = data.filter(i => i.health === queryForm.health)
if (queryForm.alarmLevel) data = data.filter(i => i.alarmLevel === queryForm.alarmLevel)
if (queryForm.line) data = data.filter(i => i.line.includes(queryForm.line))
if (queryForm.station) data = data.filter(i => i.station.includes(queryForm.station))
pageInfo.total = data.length
const start = (pageInfo.page - 1) * pageInfo.pageSize
deviceList.value = data.slice(start, start + pageInfo.pageSize)
}
const handleQuery = () => { pageInfo.page = 1; filterDeviceList() }
const resetQuery = () => {
Object.assign(queryForm, { deviceName: '', health: '', alarmLevel: '', line: '', station: '' })
pageInfo.page = 1
filterDeviceList()
}
const handleSizeChange = () => { pageInfo.page = 1; filterDeviceList() }
const handleCurrentChange = () => { filterDeviceList() }
- 弹窗表单校验(关闭清校验;保存后刷新列表):
import { ref, reactive } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
const dialogVisible = ref(false)
const deviceFormRef = ref<FormInstance>()
const deviceFormRules = reactive<FormRules>({
deviceName: [{ required: true, message: '请输入设备名称', trigger: 'blur' }],
line: [{ required: true, message: '请输入产线', trigger: 'blur' }],
station: [{ required: true, message: '请输入工位', trigger: 'blur' }],
health: [{ required: true, message: '请选择运行状态', trigger: 'change' }],
alarmLevel: [{ required: true, message: '请选择告警等级', trigger: 'change' }],
})
const handleDialogClose = () => { deviceFormRef.value?.clearValidate() }
const submitDeviceForm = async () => {
await deviceFormRef.value?.validate()
dialogVisible.value = false
getDeviceList()
}
- 筛选表单(Form inline)
- 字段建议:设备名称(输入框)、状态(Select)、位置(Select)、日期范围(可选)
- 按钮:查询、重置、新增设备
- 表格(Table)
-
列建议:编号、设备名、状态 Tag、最后心跳、位置、操作
-
操作建议:查看、编辑、启停(危险操作需确认)
-
设备编号(deviceId)
-
设备名称(deviceName)
-
产线/区域(line)
-
工位(station)
-
健康状态(health Tag)
-
告警等级(alarmLevel Tag,可与 health 合并显示)
-
最近心跳(heartbeatAt)
-
吞吐量(throughputPerMin)
-
异常率(rejectRate)
-
操作(查看/编辑/启停/告警确认)
- 分页(Pagination)
- 统一字段:
page、pageSize、total - 交互规则:筛选变化后
page = 1再查询
- 弹窗(Dialog + Form 校验)
- 新增/编辑共用一套表单模型
- 关闭弹窗时要重置表单与校验状态
- 先做纯展示:表格渲染 mock 数据 + 基础分页展示 total
- 再做查询闭环:筛选表单 → 点击查询 → 重新计算/拉取 rows,并把 page 归 1
- 再做分页闭环:切页/改 pageSize 时仍带着筛选条件刷新 rows
- 最后做弹窗闭环:新增/编辑 → 校验 → 保存 → 关闭弹窗 → 刷新列表
- 使用
el-row/el-col的断点控制(如:xs="24" :sm="12" :md="8") - 筛选区在窄屏自动换行;操作按钮区域不挤压表单
- 表格在小屏可横向滚动(必要时设置
min-width,避免列挤成一团)
把下面提示词直接发给 AI,要求输出“文件路径 + 完整代码”,生成后按审计清单自查。
提示词(设备列表页闭环):
请基于 Vue3 + Vite + TypeScript + Element Plus 技术栈,为智能分拣系统后台开发完整页面。请严格按照指定文件路径输出可直接运行的完整代码,必须包含完整 import、组件引用、类型定义、交互逻辑,禁止使用代码片段、省略号或不完整写法。
需输出文件(按顺序)
src/main.ts
全局集成 Element Plus 并引入样式,正确配置 Element Plus 中文 locale;注册 Element Plus 图标;集成 Pinia 与 Router;完成应用创建与挂载。
src/App.vue
实现标准后台布局:el-container / el-aside / el-header / el-main;
左侧菜单包含:仪表盘、设备列表;
主区域渲染路由页面;
必须使用 <script setup lang="ts">。
src/router/index.ts
创建 router(history 使用 createWebHistory(import.meta.env.BASE_URL)),导出默认 router。
src/router/routes.static.ts
定义静态路由:/ 重定向到 /dashboard;/dashboard;/device-list;未知路由重定向到 /dashboard。
src/types/device.ts
输出 Health/AlarmLevel/DeviceRow/DeviceQueryForm 四个类型定义(与下方严格一致),供页面复用。
src/views/DashboardView.vue
页面结构:4 个统计指标卡片 + 1 个图表占位区域 + 1 个最近告警表格;
允许使用 mock 数据;全部使用 Element Plus 组件实现。
src/views/DeviceListView.vue
必须完整实现:
筛选表单:deviceName /health/alarmLevel /line/station
设备表格渲染
分页与列表数据联动
操作栏:查看 / 编辑 / 设备启停 / 告警确认
新增 / 编辑共用弹窗表单,内置表单校验
强制约束
仅使用 Element Plus,不引入任何其他 UI 库
统一使用 <script setup lang="ts">,禁止使用 any 类型
严格遵循以下 TypeScript 类型定义(不可修改):
```ts
type Health = 'ok' | 'warn' | 'alarm' | 'offline'
type AlarmLevel = 'none' | 'L1' | 'L2' | 'L3'
interface DeviceRow {
deviceId: string
deviceName: string
line: string
station: string
health: Health
alarmLevel: AlarmLevel
heartbeatAt: number
throughputPerMin: number
rejectRate: string
lastErrorCode?: string
updatedAt: string
}
interface DeviceQueryForm {
deviceName: string
health: Health | ''
alarmLevel: AlarmLevel | ''
line: string
station: string
}
```
离线判断规则:当前时间戳(ms)- heartbeatAt(ms)> 180000(即 180 秒)→ 强制视为 offline,状态 Tag 与操作按钮必须联动
交互规则:
筛选 / 重置后自动回到第 1 页
保存成功后关闭弹窗并刷新列表
关闭弹窗时清空表单校验状态
输出格式
所有文件按 文件路径 + 完整代码块 输出,代码可直接复制运行;
最后提供 6 条标准化自查清单,包含:样式展示、中文国际化、筛选功能、分页联动、表单校验、危险操作确认。
审计清单(至少自查 6 条):
- Element Plus 样式是否生效(按钮/表格是否有样式)
- Pagination/Empty 等内置文案是否为中文
- 点击“重置”是否清空筛选并回到第一页
- 分页切换是否保留筛选条件
- 弹窗关闭后再次打开是否不会残留上次校验红字
- 启停/删除等危险操作是否有确认提示
- 离线设备是否置灰高风险操作并给出原因提示
加分项(可选):
- 表格长文本溢出处理(tooltip/ellipsis),重要列固定宽度
- 状态 Tag 颜色与文案统一(running/stopped/alarm/offline)
- 设备行操作收敛(超过 3 个操作用下拉菜单)
项目工坊任务:
- 完成智能分拣系统仪表盘或设备列表(至少一个达到可交付)
- 完成筛选 + 表格 + 分页 + 弹窗的交互闭环(设备列表优先)
- 完成响应式布局调优(窄屏不崩、信息仍可读)
- 页面结构清晰:分区明确(Card/标题),对齐统一
- 设备列表闭环完整:筛选、分页、弹窗编辑可用
- 操作语义正确:危险操作有确认,状态展示明确(Tag/文本)
- 响应式可用:窄屏不遮挡、不溢出、可滚动
1. 样式没生效(最常见)
- 是否引入
element-plus/dist/index.css - 是否重复引入导致样式覆盖异常(先保证只引一次)
- 是否被全局 CSS 重置破坏(检查
* {}、button {}等规则)
2. 中文/国际化不生效
- 是否
app.use(ElementPlus, { locale: zhCn }) - 是否导入了正确的语言包(
element-plus/es/locale/lang/zh-cn) - 是否只做了业务文案翻译但忘了组件库 locale
3. 表格布局抖动/难读
- 列宽是否缺省导致挤压(建议关键列固定 width)
- 长文本是否设置溢出(tooltip/ellipsis)
- 表格高度是否随内容跳(必要时固定容器高度并滚动)
4. 筛选与分页联动混乱
- 筛选变化是否把
page重置为 1 - 查询函数是否只认一份“统一条件对象”(避免散落多个 ref)
- 翻页时是否把筛选条件带上(不要只发 page 参数)
5. 弹窗表单残留/校验不正确
- 关闭弹窗时是否 reset 表单模型
- 是否清理校验状态(避免下次打开仍红字)
- 新增/编辑是否共用同一表单但未区分初始化逻辑
附录
- Element-plus官方网址:https://element-plus.org/zh-CN/component/overview.