feat: 新增预警与附件模块,优化工位派工功能
1. 新增预警规则、预警消息、业务附件数据库表与CRUD逻辑 2. 为拧紧记录添加审核人、审核时间字段及审核留痕功能 3. 优化产线点位类型与编号描述,更新工位组合下发菜单名称为工艺路线派工 4. 新增上传文件获取原文件名与大小的工具方法 5. 在系统管理菜单新增预警中心与附件中心入口
This commit is contained in:
@@ -103,7 +103,7 @@ export const helpMaterial = {
|
||||
}
|
||||
|
||||
export const helpPlc = {
|
||||
title: '工位组合下发(下发录入 / 下发记录)',
|
||||
title: '工艺路线派工(下发录入 / 下发记录)',
|
||||
overview:
|
||||
'这个菜单做什么用:把某工单/工件的「工位组合」写入产线设备(如 PLC 控制器),产线传送带据此只停靠勾选的工位,其余工位不加工。\n\n页面分两个页签:「下发录入」(默认打开)、「下发记录」(点击才加载,可按工单号/状态筛选)。\n\n操作流程:选工单号 → 下拉选出该工单已上线未完工的工件 SN(也可扫码/手输) → 系统按工件实际进度自动带出「当前工位」(只读) → 工位组合中 ≤ 当前工位的编号视为已完成,自动剔除且禁选 → 勾选剩余要走的工位 → 下发。\n\n为什么单独一个菜单:产线偶尔需要人工干预——比如本该走 3 号工位却做成了半成品,人工在此只勾剩余工位重上线,完成闭环。\n\n核心约束:系统遵循「上一条未收到完成信号,不下发下一条」,确保产线不串序。\n\n数据链路:工单(含工位组合) → 本页下发到产线设备 → 产线设备驱动传送带/工位 → 收到完成信号再下发下一条。',
|
||||
fields: [
|
||||
|
||||
@@ -63,7 +63,7 @@ import { useRouter } from 'vue-router'
|
||||
import {
|
||||
Document, Calendar, Box, Goods, ShoppingCart, Monitor, Tools,
|
||||
Operation, Link, DataLine, EditPen, Search, CircleCheck, Clock, User,
|
||||
Key, Fold, Expand
|
||||
Key, Fold, Expand, Warning, Paperclip
|
||||
} from '@element-plus/icons-vue'
|
||||
import { getUser, logout } from '../utils/auth'
|
||||
import request from '../utils/request'
|
||||
@@ -126,7 +126,7 @@ const menuGroups = [
|
||||
},
|
||||
{
|
||||
title: '现场作业', icon: Monitor, children: [
|
||||
{ code: 'produce.plc', name: '工位组合下发', path: '/plc-send', icon: Monitor },
|
||||
{ code: 'produce.plc', name: '工艺路线派工', path: '/plc-send', icon: Monitor },
|
||||
{ code: 'produce.scan', name: '手动报工', path: '/scan', icon: EditPen },
|
||||
{ code: 'produce.trace', name: '工件追溯', path: '/trace', icon: Search },
|
||||
{ code: 'produce.torque', name: '拧紧查询', path: '/torque', icon: Tools },
|
||||
@@ -145,7 +145,9 @@ const menuGroups = [
|
||||
title: '系统管理', icon: Key, children: [
|
||||
{ code: 'sys.account', name: '账号管理', path: '/account', icon: User },
|
||||
{ code: 'sys.role', name: '角色管理', path: '/role', icon: Key },
|
||||
{ code: 'sys.eventlog', name: '操作日志', path: '/event-log', icon: Clock }
|
||||
{ code: 'sys.eventlog', name: '操作日志', path: '/event-log', icon: Clock },
|
||||
{ code: 'sys.alert', name: '预警中心', path: '/alert-center', icon: Warning },
|
||||
{ code: 'sys.attachment', name: '附件中心', path: '/attachment', icon: Paperclip }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<el-card>
|
||||
<el-tabs v-model="tab">
|
||||
<el-tab-pane label="规则设置" name="rules">
|
||||
<div style="margin-bottom:12px">
|
||||
<el-button type="primary" @click="openRule()">新增规则</el-button>
|
||||
</div>
|
||||
<el-table :data="rules" border size="small">
|
||||
<el-table-column prop="name" label="规则名称" min-width="140" />
|
||||
<el-table-column label="类型" width="130">
|
||||
<template #default="{row}">{{ typeLabel(row.type) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="threshold" label="阈值" width="90" />
|
||||
<el-table-column prop="receiver" label="接收人" min-width="140" />
|
||||
<el-table-column label="启用" width="80">
|
||||
<template #default="{row}"><el-tag :type="row.enabled ? 'success' : 'info'">{{ row.enabled ? '启用' : '停用' }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdBy" label="创建人" width="100" />
|
||||
<el-table-column label="操作" width="150" fixed="right">
|
||||
<template #default="{row}">
|
||||
<el-button size="small" @click="openRule(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="delRule(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="预警消息" name="msgs">
|
||||
<div style="margin-bottom:12px;display:flex;align-items:center;gap:12px">
|
||||
<el-radio-group v-model="statusFilter" @change="loadAlerts">
|
||||
<el-radio-button label="all">全部</el-radio-button>
|
||||
<el-radio-button label="UNREAD">未读</el-radio-button>
|
||||
<el-radio-button label="READ">已读</el-radio-button>
|
||||
</el-radio-group>
|
||||
<span style="color:#888">未读:{{ unread }}</span>
|
||||
</div>
|
||||
<el-table :data="alerts" border size="small">
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{row}"><el-tag :type="row.status==='UNREAD' ? 'danger' : 'info'">{{ row.status==='UNREAD' ? '未读' : '已读' }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="type" label="类型" width="130">
|
||||
<template #default="{row}">{{ typeLabel(row.type) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="title" label="标题" min-width="140" />
|
||||
<el-table-column prop="content" label="内容" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="refId" label="关联ID" width="120" />
|
||||
<el-table-column prop="createdAt" label="时间" min-width="170" />
|
||||
<el-table-column label="操作" width="110" fixed="right">
|
||||
<template #default="{row}">
|
||||
<el-button size="small" :disabled="row.status==='READ'" @click="markRead(row)">标记已读</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<el-dialog v-model="ruleVisible" :title="ruleForm.id ? '编辑规则' : '新增规则'" width="460px">
|
||||
<el-form :model="ruleForm" label-width="90px">
|
||||
<el-form-item label="规则名称"><el-input v-model="ruleForm.name" placeholder="必填" /></el-form-item>
|
||||
<el-form-item label="预警类型">
|
||||
<el-select v-model="ruleForm.type" style="width:100%">
|
||||
<el-option v-for="t in types" :key="t.value" :label="t.label" :value="t.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="阈值">
|
||||
<el-input-number v-model="ruleForm.threshold" :min="0" :step="1" />
|
||||
<span style="margin-left:8px;color:#888">拧紧/检验=不合格次数阈值;库存偏低=库存上限</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="接收人"><el-input v-model="ruleForm.receiver" placeholder="逗号分隔,如 张三,李四" /></el-form-item>
|
||||
<el-form-item label="启用"><el-switch v-model="ruleForm.enabled" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="ruleVisible=false">取消</el-button>
|
||||
<el-button type="primary" @click="saveRule">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</template>
|
||||
<script setup>
|
||||
import { reactive, ref, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import request from '../utils/request'
|
||||
|
||||
const tab = ref('rules')
|
||||
const rules = ref([])
|
||||
const alerts = ref([])
|
||||
const unread = ref(0)
|
||||
const statusFilter = ref('all')
|
||||
const ruleVisible = ref(false)
|
||||
|
||||
const types = [
|
||||
{ value: 'torque_ng', label: '拧紧不合格' },
|
||||
{ value: 'inspection_ng', label: '检验不合格' },
|
||||
{ value: 'inventory_low', label: '库存偏低' },
|
||||
{ value: 'task_overdue', label: '工单超时' }
|
||||
]
|
||||
function typeLabel(v) {
|
||||
const t = types.find((x) => x.value === v)
|
||||
return t ? t.label : v
|
||||
}
|
||||
|
||||
const ruleForm = reactive({ id: 0, name: '', type: 'torque_ng', threshold: 1, receiver: '', enabled: true })
|
||||
|
||||
async function loadRules() {
|
||||
rules.value = (await request.get('/alert/rules')) || []
|
||||
}
|
||||
async function loadAlerts() {
|
||||
const status = statusFilter.value === 'all' ? '' : statusFilter.value
|
||||
alerts.value = (await request.get('/alerts', { params: { status } })) || []
|
||||
unread.value = (await request.get('/alerts', { params: { status: 'UNREAD' } }))?.length || 0
|
||||
}
|
||||
function openRule(row) {
|
||||
if (row) Object.assign(ruleForm, { id: row.id, name: row.name, type: row.type, threshold: row.threshold, receiver: row.receiver, enabled: row.enabled })
|
||||
else Object.assign(ruleForm, { id: 0, name: '', type: 'torque_ng', threshold: 1, receiver: '', enabled: true })
|
||||
ruleVisible.value = true
|
||||
}
|
||||
async function saveRule() {
|
||||
if (!ruleForm.name) return ElMessage.warning('规则名称必填')
|
||||
const body = { name: ruleForm.name, type: ruleForm.type, threshold: ruleForm.threshold, receiver: ruleForm.receiver, enabled: ruleForm.enabled }
|
||||
if (ruleForm.id) {
|
||||
body.id = ruleForm.id
|
||||
await request.put('/alert/rule', body)
|
||||
} else {
|
||||
await request.post('/alert/rule', body)
|
||||
}
|
||||
ElMessage.success('保存成功')
|
||||
ruleVisible.value = false
|
||||
loadRules()
|
||||
}
|
||||
async function delRule(row) {
|
||||
await ElMessageBox.confirm('确认删除该规则?', '提示', { type: 'warning' })
|
||||
await request.delete('/alert/rule?id=' + row.id)
|
||||
ElMessage.success('已删除')
|
||||
loadRules()
|
||||
}
|
||||
async function markRead(row) {
|
||||
await request.post('/alert/read', { id: row.id })
|
||||
loadAlerts()
|
||||
}
|
||||
onMounted(() => { loadRules(); loadAlerts() })
|
||||
</script>
|
||||
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<el-card>
|
||||
<el-form inline style="margin-bottom:12px">
|
||||
<el-form-item label="业务类型">
|
||||
<el-select v-model="form.bizType" style="width:160px">
|
||||
<el-option label="工单" value="work_order" />
|
||||
<el-option label="物料" value="material" />
|
||||
<el-option label="工艺流程" value="process_flow" />
|
||||
<el-option label="检验" value="inspection" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="业务ID">
|
||||
<el-input v-model="form.bizId" placeholder="如 工单号 / 物料编码" style="width:220px" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-upload action="#" :show-file-list="false" :http-request="doUpload">
|
||||
<el-button type="primary">选择文件上传</el-button>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
<el-form-item><el-button @click="load">查询</el-button></el-form-item>
|
||||
</el-form>
|
||||
<el-table :data="list" border size="small">
|
||||
<el-table-column prop="fileName" label="文件名" min-width="180" />
|
||||
<el-table-column prop="fileSize" label="大小" width="110">
|
||||
<template #default="{row}">{{ fmtSize(row.fileSize) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="bizType" label="业务类型" width="120" />
|
||||
<el-table-column prop="bizId" label="业务ID" width="140" />
|
||||
<el-table-column prop="uploader" label="上传人" width="100" />
|
||||
<el-table-column prop="createdAt" label="上传时间" min-width="170" />
|
||||
<el-table-column label="操作" width="160" fixed="right">
|
||||
<template #default="{row}">
|
||||
<el-button size="small" type="primary" @click="download(row)">下载</el-button>
|
||||
<el-button size="small" type="danger" @click="remove(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</template>
|
||||
<script setup>
|
||||
import { reactive, ref, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import request from '../utils/request'
|
||||
import { getToken } from '../utils/auth'
|
||||
|
||||
const form = reactive({ bizType: 'work_order', bizId: '' })
|
||||
const list = ref([])
|
||||
|
||||
async function load() {
|
||||
list.value = (await request.get('/attachments', { params: { bizType: form.bizType, bizId: form.bizId } })) || []
|
||||
}
|
||||
function fmtSize(n) {
|
||||
n = n || 0
|
||||
if (n < 1024) return n + ' B'
|
||||
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB'
|
||||
return (n / 1024 / 1024).toFixed(1) + ' MB'
|
||||
}
|
||||
async function doUpload(option) {
|
||||
if (!form.bizId) {
|
||||
ElMessage.warning('请先填写业务ID再上传')
|
||||
return
|
||||
}
|
||||
const fd = new FormData()
|
||||
fd.append('file', option.file)
|
||||
fd.append('bizType', form.bizType)
|
||||
fd.append('bizId', form.bizId)
|
||||
try {
|
||||
await request.post('/attachment/upload', fd)
|
||||
ElMessage.success('上传成功')
|
||||
if (option.onSuccess) option.onSuccess()
|
||||
load()
|
||||
} catch (e) {
|
||||
if (option.onError) option.onError(e)
|
||||
}
|
||||
}
|
||||
async function download(row) {
|
||||
const token = getToken()
|
||||
const resp = await fetch('/api/v1/attachment/download?name=' + encodeURIComponent(row.filePath), { headers: { Authorization: 'Bearer ' + token } })
|
||||
if (!resp.ok) { ElMessage.error('下载失败'); return }
|
||||
const blob = await resp.blob()
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = row.fileName || 'file'
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
async function remove(row) {
|
||||
await ElMessageBox.confirm('确认删除该附件?', '提示', { type: 'warning' })
|
||||
await request.post('/attachment/delete', { id: row.id })
|
||||
ElMessage.success('已删除')
|
||||
load()
|
||||
}
|
||||
onMounted(load)
|
||||
</script>
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-card>
|
||||
<el-alert type="info" :closable="false" style="margin-bottom:12px"
|
||||
title="工位组合下发:把某工单/工件要经过的「工位组合」写入产线设备(如 PLC 控制器),产线传送带据此只停靠对应工位。系统保证「上一条未收到完成信号前不下发下一条」,避免串序。" />
|
||||
title="工艺路线派工:把某工单/工件要经过的「工位组合」写入产线设备(如 PLC 控制器),产线传送带据此只停靠对应工位。系统保证「上一条未收到完成信号前不下发下一条」,避免串序。" />
|
||||
|
||||
<el-tabs v-model="tab" @tab-change="onTab">
|
||||
<!-- Tab1 下发录入(前,默认打开) -->
|
||||
|
||||
@@ -22,7 +22,19 @@
|
||||
</el-table-column>
|
||||
<el-table-column prop="operator" label="操作人" width="110" />
|
||||
<el-table-column prop="time" label="时间" min-width="170" />
|
||||
</el-table>
|
||||
<el-table-column prop="auditBy" label="审核人" width="100">
|
||||
<template #default="{row}"><el-tag v-if="row.auditBy" type="success">{{ row.auditBy }}</el-tag><span v-else style="color:#bbb">未审核</span></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="auditAt" label="审核时间" width="170">
|
||||
<template #default="{row}">{{ row.auditAt || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="170" fixed="right">
|
||||
<template #default="{row}">
|
||||
<el-button size="small" type="primary" :disabled="!!row.auditBy" @click="openAudit(row)">审核签字</el-button>
|
||||
<el-button size="small" @click="openLog(row)">留痕</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- Tab2 拧紧补录(录入在前默认打开,定位=设备漏传/手工修正,实时采集在工位终端/产线设备) -->
|
||||
@@ -47,6 +59,40 @@
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<el-dialog v-model="auditVisible" title="拧紧记录审核签字" width="460px" @closed="auditRemark=''">
|
||||
<el-descriptions :column="1" border size="small" v-if="auditRow">
|
||||
<el-descriptions-item label="SN">{{ auditRow.sn }}</el-descriptions-item>
|
||||
<el-descriptions-item label="工单号">{{ auditRow.workOrderNo }}</el-descriptions-item>
|
||||
<el-descriptions-item label="工位">{{ auditRow.stationNo }}</el-descriptions-item>
|
||||
<el-descriptions-item label="螺丝号">{{ auditRow.screwNo || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="扭矩">{{ auditRow.torque }}</el-descriptions-item>
|
||||
<el-descriptions-item label="结果">
|
||||
<el-tag :type="auditRow.result==='OK'?'success':'danger'">{{ auditRow.result==='OK'?'合格':'不合格' }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-form label-width="80px" style="margin-top:12px">
|
||||
<el-form-item label="审核备注"><el-input v-model="auditRemark" type="textarea" placeholder="选填,如:已复核扭矩曲线" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="auditVisible=false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmAudit">确认审核</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-drawer v-model="logVisible" title="修改留痕" size="520px">
|
||||
<el-timeline v-if="logs.length">
|
||||
<el-timeline-item v-for="(l,i) in logs" :key="i" :timestamp="l.createdAt" placement="top">
|
||||
<el-tag size="small" type="info">{{ l.action }}</el-tag>
|
||||
<span style="margin-left:6px">{{ l.operator }}</span>
|
||||
<div v-if="l.fieldName" style="color:#888">字段:{{ l.fieldName }}</div>
|
||||
<div v-if="l.oldVal || l.newVal" style="color:#888">改前:{{ l.oldVal || '-' }} → 改后:{{ l.newVal || '-' }}</div>
|
||||
<div v-if="l.remark" style="color:#888">备注:{{ l.remark }}</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
<el-empty v-else description="暂无留痕" />
|
||||
</el-drawer>
|
||||
|
||||
</el-card>
|
||||
<PageHelp :data="helpTorque" page="Torque" />
|
||||
</template>
|
||||
@@ -78,5 +124,28 @@ async function submit() {
|
||||
Object.assign(torque, { sn: '', workOrderNo: '', stationNo: '', screwNo: '', strain: 20, angle: 90, result: 'OK', reason: '' })
|
||||
list.value = []
|
||||
}
|
||||
const auditVisible = ref(false)
|
||||
const auditRow = ref(null)
|
||||
const auditRemark = ref('')
|
||||
const logVisible = ref(false)
|
||||
const logs = ref([])
|
||||
|
||||
function openAudit(row) {
|
||||
auditRow.value = row
|
||||
auditRemark.value = ''
|
||||
auditVisible.value = true
|
||||
}
|
||||
async function confirmAudit() {
|
||||
if (!auditRow.value) return
|
||||
await request.post('/torque/audit', { recordId: auditRow.value.id, remark: auditRemark.value })
|
||||
ElMessage.success('审核成功')
|
||||
auditVisible.value = false
|
||||
load()
|
||||
}
|
||||
async function openLog(row) {
|
||||
logs.value = (await request.get('/torque/audit-log', { params: { recordId: row.id } })) || []
|
||||
logVisible.value = true
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
@@ -12,7 +12,7 @@ const routes = [
|
||||
{ path: 'daily-plan', name: 'DailyPlan', component: () => import('../pages/DailyPlan.vue'), meta: { title: '日排产' } },
|
||||
{ path: 'bom', name: 'Bom', component: () => import('../pages/Bom.vue'), meta: { title: '物料清单' } },
|
||||
{ path: 'material-request', name: 'MaterialRequest', component: () => import('../pages/MaterialRequest.vue'), meta: { title: '备料单' } },
|
||||
{ path: 'plc-send', name: 'PlcSend', component: () => import('../pages/PlcSend.vue'), meta: { title: '工位组合下发' } },
|
||||
{ path: 'plc-send', name: 'PlcSend', component: () => import('../pages/PlcSend.vue'), meta: { title: '工艺路线派工' } },
|
||||
{ path: 'torque', name: 'Torque', component: () => import('../pages/Torque.vue'), meta: { title: '拧紧查询' } },
|
||||
{ path: 'process-flow', name: 'ProcessFlow', component: () => import('../pages/ProcessFlow.vue'), meta: { title: '工艺流程' } },
|
||||
{ path: 'station', name: 'Station', component: () => import('../pages/Station.vue'), meta: { title: '关联工位' } },
|
||||
@@ -25,7 +25,9 @@ const routes = [
|
||||
{ path: 'event-log', name: 'EventLog', component: () => import('../pages/EventLog.vue'), meta: { title: '操作日志' } },
|
||||
{ path: 'change-password', name: 'ChangePassword', component: () => import('../pages/ChangePassword.vue'), meta: { title: '修改密码' } },
|
||||
{ path: 'account', name: 'AccountManage', component: () => import('../pages/AccountManage.vue'), meta: { title: '账号管理' } },
|
||||
{ path: 'role', name: 'RoleManage', component: () => import('../pages/RoleManage.vue'), meta: { title: '角色管理' } }
|
||||
{ path: 'role', name: 'RoleManage', component: () => import('../pages/RoleManage.vue'), meta: { title: '角色管理' } },
|
||||
{ path: 'alert-center', name: 'AlertCenter', component: () => import('../pages/AlertCenter.vue'), meta: { title: '预警中心' } },
|
||||
{ path: 'attachment', name: 'Attachment', component: () => import('../pages/Attachment.vue'), meta: { title: '附件中心' } },
|
||||
]
|
||||
},
|
||||
{ path: '/:pathMatch(.*)*', redirect: '/work-order' }
|
||||
|
||||
Reference in New Issue
Block a user