特种作业现场管不好,事故就来敲门。本文完整复盘一个真实的工业安全场景 SaaS 前端项目,涵盖多级联动、复杂表单校验、文件上传与 Mock 机制,手把手带你从项目结构设计到生产对接。
一、业务背景与痛点
工业场景下的特种作业(高处作业、动火作业、受限空间作业等),如果管理松散,轻则罚款整改,重则人员伤亡。传统的管理方式靠纸质作业票,靠人工登记违章记录,查找困难、追溯更难。
这个项目解决的核心问题是:把"谁在什么时间、什么地点、干了什么违章的事、罚了多少钱"这件事,用系统管起来。
目标用户是一线的安全管理人员------他们不需要懂技术,只需要:
- 能快速录一条违章记录
- 能按时间、人员、违章类型等多个维度查询历史记录
- 能看到完整的处罚过程和附件证据
二、技术选型与项目结构
2.1 核心技术栈
| 技术 | 版本 | 用途 |
|---|---|---|
| Vue 3 | ^3.4.0 | 核心框架,Composition API |
| TypeScript | ^5.3.0 | 类型安全 |
| Element Plus | ^2.5.0 | UI 组件库 |
| Vue Router | ^4.2.0 | 路由管理,动态导入 |
| Pinia | ^2.1.0 | 状态管理(本项目暂未深度使用) |
| Axios | ^1.6.0 | HTTP 请求 |
| Vite | ^5.0.0 | 构建工具 |
| SCSS | ^1.69.0 | 样式预处理 |
2.2 项目结构
code复制
bash
src/
├── api/ # 所有接口调用,统一管理
│ └── violation.ts
├── directives/ # 自定义指令(如 v-permission 权限控制)
├── layouts/ # 布局组件
├── router/ # 路由配置(路由懒加载)
├── types/ # TypeScript 类型定义
│ └── violation.ts
├── utils/ # 工具函数(axios 封装、通用函数)
│ └── request.ts
├── views/ # 页面视图
│ └── violation/
│ ├── ViolationList.vue # 列表页
│ ├── ViolationAdd.vue # 新增页
│ └── components/
│ ├── ViolationDetailDialog.vue # 详情弹窗
│ └── SpecialWorkDialog.vue # 作业票选择弹窗
├── App.vue
└── main.ts
设计原则:
- 一个功能一个文件夹,按模块划分,不把全部页面堆在一起
- 类型定义和接口调用严格分开,修改接口不影响组件
- 组件用
<script setup lang="ts">语法,代码量减少约 30%
三、三大核心模块设计与实现
3.1 模块一:违章记录列表页(多条件搜索 + 分页)
列表页是安全管理人员每天都会打开的页面,核心诉求是:快、准、稳。
搜索表单设计
一个违章记录有 9 个可筛选项:违章名称、项目名称、承包商、违章作业类型、违章地点、违章人员、违章行为描述、处罚措施、时间范围。
vue复制
ini
<el-form :model="searchForm" ref="searchFormRef" class="search-form">
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="违章名称">
<el-input v-model="searchForm.violationName" placeholder="请输入" clearable />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="违章作业">
<el-select v-model="searchForm.violationWork" placeholder="不限" clearable>
<el-option label="高处作业" value="高处作业" />
<el-option label="动火作业" value="动火作业" />
<el-option label="受限空间作业" value="受限空间作业" />
</el-select>
</el-form-item>
</el-col>
<!-- 时间范围单独一行 -->
<el-col :span="6">
<el-form-item label="违章时间">
<el-date-picker
v-model="searchForm.timeRange"
type="daterange"
range-separator="---"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
搜索与重置逻辑
typescript复制
ini
const fetchList = async () => {
loading.value = true
try {
const params: ViolationListParams = {
pageNum: pagination.pageNum,
pageSize: pagination.pageSize,
violationName: searchForm.violationName || undefined,
violationWork: searchForm.violationWork || undefined,
startTime: searchForm.timeRange?.[0],
endTime: searchForm.timeRange?.[1]
// 其他筛选项同理...
}
const res = await getViolationList(params)
tableData.value = res.list
pagination.total = res.total
} finally {
loading.value = false
}
}
// 搜索:回到第1页再查
const handleSearch = () => {
pagination.pageNum = 1
fetchList()
}
// 重置:清空表单 + 回到第1页
const handleReset = () => {
searchFormRef.value?.resetFields()
pagination.pageNum = 1
fetchList()
}
经验: 搜索和重置都要把
pageNum重置为 1,这是最容易遗漏的细节------用户搜完忘记清空,结果看到空列表,还以为是系统 Bug。
3.2 模块二:新增违章页(级联选择 + 复合处罚 + 文件上传)
这是复杂度最高的页面,涉及多个联动逻辑和表单校验。
承包商 → 项目级联选择
承包商和项目是父子级联关系:选了承包商,才会出现对应的项目列表;换承包商,项目和作业人员都要清空。
typescript复制
ini
const handleContractorChange = async (val: string) => {
// 切换承包商时,重置下游字段
form.projectId = ''
form.projectName = ''
if (val) {
// 根据新承包商ID加载项目列表
projectOptions.value = await getProjectOptions(val)
// 记录承包商名称(用于后续提交)
const contractor = contractorOptions.value.find((item: any) => item.id === val)
form.contractorName = contractor?.name || ''
}
}
级联选择的 UI 用的是 Element Plus 原生 el-select,不依赖第三方级联组件,保持了极低的依赖复杂度。
违章名称实时防抖校验
违章名称要求全局唯一(同一个项目下不能有重名违章)。用防抖 + 异步 API 来做实时校验:
typescript复制
typescript
let nameCheckTimer: ReturnType<typeof setTimeout> | null = null
function validateViolationName(_rule: any, value: string, callback: any) {
if (!value) { callback(); return }
// 防抖:500ms 内重复输入只发一次请求
if (nameCheckTimer) clearTimeout(nameCheckTimer)
nameCheckTimer = setTimeout(async () => {
try {
const res = await checkViolationNameUnique(value)
if (!res.unique) {
callback(new Error('该违章名称已存在,请使用其他名称'))
} else {
callback()
}
} catch {
callback(new Error('校验失败,请重试'))
}
}, 500)
}
复合处罚措施设计
处罚方式有 5 种,且可以叠加------既罚款又停工整改是常见场景。设计成复选框 + 条件显示输入框:
vue复制
ini
<el-checkbox v-model="form.punishmentWarning">警告</el-checkbox>
<el-checkbox v-model="form.punishmentFine">
罚款
<el-input-number
v-if="form.punishmentFine"
v-model="form.punishmentFineAmount"
:precision="1"
:min="0"
controls-position="right"
style="width: 100px"
/>
</el-checkbox>
提交时,用 buildPunishmentMeasure() 函数把多个处罚项合并成一个展示字符串:
typescript复制
kotlin
function buildPunishmentMeasure(data: ViolationForm): string {
const measures: string[] = []
if (data.punishmentWarning) measures.push('警告')
if (data.punishmentFine && data.punishmentFineAmount)
measures.push(`罚款${data.punishmentFineAmount}元`)
if (data.punishmentDeduct && data.punishmentDeductScore)
measures.push(`扣分${data.punishmentDeductScore}分`)
if (data.punishmentStop) measures.push('停工整改')
if (data.punishmentOther && data.punishmentOtherContent)
measures.push(data.punishmentOtherContent)
return measures.join('、') || '-'
}
文件上传触发表单验证
文件上传有点特殊------它是手动点按钮触发 on-change 回调上传,上传成功后才把 URL 填入表单。由于 el-form-item 的 prop 绑定的是 form.violationPhotos 数组,所以上传成功后要主动触发一次校验:
typescript复制
javascript
const handleFileChange = async (file: UploadFile) => {
if (file.raw) {
try {
const res = await uploadViolationPhoto(file.raw)
form.violishmentPhotos.push(res.url)
// 关键:上传成功后手动触发表单校验
formRef.value?.validateField('violationPhotos')
} catch {
ElMessage.error('上传失败')
}
}
}
3.3 模块三:特殊作业选择弹窗
违章记录必须关联到一张具体的特殊作业票(高处作业票、动火作业票等),不能凭空创建。这个弹窗的设计要点是:
- 单行选中 :表格配置
highlight-current-row,点击一行高亮选中 - 多条件筛选:作业类型、编号、内容、报备人、状态、日期范围等 7 个筛选项
- 状态标签 :用
<el-tag>展示审核状态,不同状态配不同颜色
vue复制
sql
<el-table
highlight-current-row
@current-change="handleCurrentChange"
>
<el-table-column prop="status" label="状态">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">{{ row.status }}</el-tag>
</template>
</el-table-column>
</el-table>
typescript复制
lua
const getStatusType = (status: string) => ({
'待审核': 'info',
'审核通过': 'success',
'验收中': 'warning',
'验收完成': 'success'
}[status] || 'info')
四、TypeScript 类型体系设计
类型定义是本项目的亮点之一。违章记录涉及 3 种核心类型,边界清晰:
typescript复制
typescript
// 列表页展示项(轻量,只含展示字段)
export interface ViolationRecord {
id: string
violationName: string
projectName: string
violationWork: string
contractorName: string
violationLocation: string
violationPerson: string
punishmentMeasure: string // 处罚措施(合并后的展示文本)
violationTime: string
}
// 详情页(包含所有字段,含数组和布尔值)
export interface ViolationDetail extends ViolationRecord {
projectId: string
violationWorkId: string
violationPersonIds: string[] // 列表是逗号拼接字符串,详情是数组
violationRule: string
possibleConsequence: string
punishmentResult: string
recorderName: string
violationPhotos: string[] // 图片URL列表
// 处罚明细(布尔 + 数值分离)
punishmentWarning: boolean
punishmentFine: boolean
punishmentFineAmount: number
// ...
}
// 新增表单(不含 id,不含人员名称,只有 ID 和布尔值)
export interface ViolationForm {
contractorId: string
projectId: string
violationPersonIds: string[]
punishmentWarning: boolean
punishmentFine: boolean
punishmentFineAmount?: number
// ...
}
设计思路: 列表 → 详情 → 表单,三种类型职责单一,避免一个类型塞入过多字段导致冗余。
五、Mock 数据与真实接口的平滑过渡
开发阶段没有后端接口,所有接口用 Promise + setTimeout 模拟。关键设计:Mock 逻辑和真实接口的调用方式完全一致,切换时只需要注释掉 Mock 行、取消注释真实请求行,不需要改任何调用方代码。
typescript复制
javascript
export function getViolationList(params: ViolationListParams) {
// ✅ 对接时:删除下面3行,取消注释上面2行即可
// return request.get('/api/violation/list', { params })
// Mock 实现
return new Promise((resolve) => {
setTimeout(() => {
// ...过滤、分页逻辑
resolve({ list: [], total: 0, pageNum: 1, pageSize: 10 })
}, 300)
})
}
接口代理配置在 vite.config.ts 中,开发环境走 /api 前缀:
typescript复制
javascript
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^/api/, '')
}
}
六、权限指令封装
系统中不同角色的用户能看到的功能不同。通过自定义 v-permission 指令控制按钮级别的权限:
vue复制
bash
<el-button v-permission="'violation:add'" type="primary" @click="handleAdd">
新增
</el-button>
权限列表从 localStorage.permissions 中读取,指令实现的核心逻辑是:根据指令值查找权限列表中是否存在,不存在则隐藏元素。
七、总结与改进方向
已实现
- ✅ 多条件搜索表单(9个筛选项 + 时间范围)
- ✅ 承包商 ↔ 项目级联选择,联动清空逻辑
- ✅ 违章作业票弹窗选择(7个筛选项)
- ✅ 复合处罚措施(多选 + 条件输入框)
- ✅ 文件上传与表单验证联动
- ✅ 违章名称防抖唯一性校验
- ✅ 完整 TypeScript 类型体系
- ✅ Mock → 真实接口平滑过渡
- ✅ 权限指令封装
改进方向
- 状态管理:引入 Pinia,把表单状态和搜索状态集中管理,支持页面刷新后恢复
- 编辑功能:当前只有新增和查看,编辑功能可以复用新增页面的表单结构
- 导出功能:列表页加导出 Excel,支持把查询结果导出存档
- 批量操作:支持批量选择记录后批量处理(如批量标记已整改)
- 消息通知:违章记录新增后,通过 WebSocket 推送给相关安全管理人员
项目地址 :violation-record-system
如果你觉得这篇文章有帮助,欢迎点赞、收藏!有任何问题欢迎评论区交流 👇