Files
bj_power/bj_power_mes/frontend/src/pages/ProcessFlow.vue
T

417 lines
17 KiB
Vue
Raw Normal View History

<template>
<el-card>
<el-form inline>
<el-form-item label="工位号">
<el-select v-model="query.stationNo" placeholder="全部" clearable style="width:140px" @change="loadFlows">
<el-option v-for="i in 12" :key="i" :label="`工位${i}`" :value="i" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="loadFlows">查询</el-button>
</el-form-item>
<el-form-item>
<el-button v-if="can('produce.processflow:add')" type="success" @click="openFlow()">新增流程</el-button>
</el-form-item>
</el-form>
<el-table :data="flows" border size="small">
<el-table-column prop="id" label="ID" width="60" />
<el-table-column prop="name" label="流程名" min-width="130" show-overflow-tooltip />
<el-table-column prop="stationNo" label="工位号" width="90" align="center" />
<el-table-column label="PDF" width="90" align="center">
<template #default="{ row }">
<el-button v-if="row.pdfFile" link type="primary" @click="previewPdf(row.pdfFile)">预览</el-button>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="状态" width="90" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 'ACTIVE' ? 'success' : 'info'">{{ statusText(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="绑定工位" min-width="120">
<template #default="{ row }">{{ (row.stations || []).map(s => `工位${s}`).join('、') || '-' }}</template>
</el-table-column>
<el-table-column label="步骤" min-width="120">
<template #default="{ row }">
<el-tooltip
v-if="row.steps && row.steps.length"
:content="row.steps.map(s => `${s.seq}.${s.name}`).join(' ')">
<span>{{ row.steps.length }} </span>
</el-tooltip>
<span v-else>0 </span>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
<el-table-column label="创建时间" width="150">
<template #default="{ row }">{{ fmt(row.createdAt) }}</template>
</el-table-column>
<el-table-column label="操作" width="220" align="center" fixed="right">
<template #default="{ row }">
<el-button
v-if="can('produce.processflow:add') || can('produce.processflow:edit')"
size="small" @click="openFlow(row)">编辑</el-button>
<el-button
v-if="can('produce.processflow:edit')"
size="small" :type="row.status === 'ACTIVE' ? 'danger' : 'success'" @click="toggleStatus(row)">
{{ row.status === 'ACTIVE' ? '停用' : '启用' }}
</el-button>
<el-button v-if="can('produce.processflow:delete')" size="small" type="danger" @click="delFlow(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- ============ 工艺流程 新增/编辑 ============ -->
<el-dialog v-model="flowDlg.show" :title="flowDlg.id ? '编辑工艺流程' : '新增工艺流程'" width="940px" top="6vh">
<el-form :model="flowDlg" label-width="90px">
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="流程名" required>
<el-input v-model="flowDlg.name" placeholder="如 3号工位装配流程" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="工位号" required>
<el-select v-model="flowDlg.stationNo" placeholder="请选择工位号(固定12个)" style="width:100%">
<el-option v-for="i in 12" :key="i" :label="`工位${i}`" :value="i" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="12">
<el-col :span="12">
<el-form-item label="状态">
<el-select v-model="flowDlg.status" placeholder="请选择状态" style="width:100%">
<el-option label="启用" value="ACTIVE" />
<el-option label="停用" value="INACTIVE" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="作业指导PDF">
<el-button
v-if="can('produce.processflow:upload')"
type="primary" plain :loading="flowDlg.uploading" @click="pickPdf">
上传 PDF
</el-button>
<span v-if="flowDlg.pdfFile" class="pdf-name">
{{ flowDlg.pdfFile }}
<el-button link type="primary" @click="previewPdf(flowDlg.pdfFile)">预览</el-button>
</span>
<input ref="pdfInput" type="file" accept="application/pdf" style="display:none" @change="onPdfChange" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="备注">
<el-input v-model="flowDlg.remark" placeholder="备注(可空)" />
</el-form-item>
<!-- 工序步骤子表 -->
<el-form-item label="工序步骤">
<div style="width:100%">
<el-table :data="flowDlg.steps" border size="small">
<el-table-column label="步骤" width="80" align="center">
<template #default="{ row }">
<el-input-number v-model="row.seq" :min="1" size="small" controls-position="right" style="width:70px" />
</template>
</el-table-column>
<el-table-column label="步骤名称" min-width="140">
<template #default="{ row }"><el-input v-model="row.name" placeholder="如 拧紧扭矩" /></template>
</el-table-column>
<el-table-column label="采集方式" width="140">
<template #default="{ row }">
<el-select v-model="row.collectType" placeholder="请选择">
<el-option value="AUTO" label="拧紧枪自动" />
<el-option value="MANUAL" label="手填" />
<el-option value="NONE" label="仅记录" />
</el-select>
</template>
</el-table-column>
<el-table-column label="是否拧紧" width="90" align="center">
<template #default="{ row }"><el-switch v-model="row.isTorque" /></template>
</el-table-column>
<el-table-column label="备注" min-width="120">
<template #default="{ row }"><el-input v-model="row.remark" placeholder="备注(可空)" /></template>
</el-table-column>
<el-table-column label="考核标准" width="110" align="center">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="openCriteria(row)">
{{ (row.criteria || []).length }}
</el-button>
</template>
</el-table-column>
<el-table-column label="操作" width="70" align="center">
<template #default="{ row }">
<el-button link type="danger" size="small" @click="removeStep(row)"></el-button>
</template>
</el-table-column>
</el-table>
<el-button size="small" style="margin-top:10px" @click="addStep">新增步骤</el-button>
</div>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="flowDlg.show = false">取消</el-button>
<el-button
v-if="can('produce.processflow:add') || can('produce.processflow:edit')"
type="primary" @click="saveFlow">保存</el-button>
</template>
</el-dialog>
<!-- ============ 考核标准编辑每步骤独立弹窗单独保存 ============ -->
<el-dialog v-model="critDlg.show" :title="`考核标准 - ${critDlg.stepName || ''}`" width="720px" top="10vh" append-to-body>
<el-alert type="info" :closable="false" style="margin-bottom:12px"
title="为「步骤」设置合格判定标准:报工时按此标准自动判定合格/不合格;未设标准则该步骤仅记录、不判定。" />
<el-table :data="critDlg.criteria" border size="small">
<el-table-column label="指标名" min-width="120">
<template #default="{ row }"><el-input v-model="row.name" placeholder="如 扭矩" /></template>
</el-table-column>
<el-table-column label="单位" width="80">
<template #default="{ row }"><el-input v-model="row.unit" placeholder="N·m" /></template>
</el-table-column>
<el-table-column label="逻辑" width="120">
<template #default="{ row }">
<el-select v-model="row.logic" placeholder="请选择">
<el-option value="GE" label="≥ 大于等于" />
<el-option value="LE" label="≤ 小于等于" />
<el-option value="GT" label="> 大于" />
<el-option value="LT" label="< 小于" />
<el-option value="RANGE" label="区间" />
<el-option value="EQUAL" label="= 等于" />
</el-select>
</template>
</el-table-column>
<el-table-column v-if="needsTarget" label="目标值" width="110">
<template #default="{ row }">
<el-input-number v-model="row.target" :step="1" controls-position="right" style="width:100%" />
</template>
</el-table-column>
<el-table-column v-if="needsRange" label="下限" width="110">
<template #default="{ row }">
<el-input-number v-model="row.min" :step="1" controls-position="right" style="width:100%" />
</template>
</el-table-column>
<el-table-column v-if="needsRange" label="上限" width="110">
<template #default="{ row }">
<el-input-number v-model="row.max" :step="1" controls-position="right" style="width:100%" />
</template>
</el-table-column>
<el-table-column label="操作" width="70" align="center">
<template #default="{ row }">
<el-button link type="danger" size="small" @click="removeCriterion(row)"></el-button>
</template>
</el-table-column>
</el-table>
<el-button size="small" style="margin-top:10px" @click="addCriterion">新增标准行</el-button>
<template #footer>
<el-button @click="critDlg.show = false">取消</el-button>
<el-button type="primary" @click="saveCriteria">确定</el-button>
</template>
</el-dialog>
</el-card>
<PageHelp :data="helpProcessFlow" />
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import PageHelp from '../components/PageHelp.vue'
import { helpProcessFlow } from '../help'
import { ElMessage, ElMessageBox } from 'element-plus'
import request from '../utils/request'
const flows = ref([])
const query = reactive({ stationNo: undefined })
const pdfInput = ref(null)
/* ---------- 权限 ---------- */
function can(code) {
try {
const u = JSON.parse(localStorage.getItem('user') || 'null')
if (!u) return false
const owned = u.permissionCodes || []
return u.roleCode === 'SUPER_ADMIN' || owned.includes('*') || owned.includes(code)
} catch {
return false
}
}
function statusText(s) {
return s === 'ACTIVE' ? '启用' : '停用'
}
/* ---------- 工艺流程 ---------- */
function blankStep(seq) {
return {
_key: Date.now() + Math.random(),
seq,
name: '',
collectType: 'MANUAL',
isTorque: false,
remark: '',
criteria: []
}
}
async function loadFlows() {
const params = {}
if (query.stationNo) params.stationNo = query.stationNo
flows.value = (await request.get('/process-flows', { params })) || []
}
function fmt(v) { return v ? String(v).replace('T', ' ').slice(0, 19) : '-' }
function previewPdf(pdfFile) {
if (pdfFile) window.open('/api/v1/files/' + pdfFile, '_blank')
}
function pickPdf() {
if (pdfInput.value) pdfInput.value.click()
}
async function onPdfChange(e) {
const file = e.target.files && e.target.files[0]
e.target.value = ''
if (!file) return
if (file.type !== 'application/pdf' && !/\.pdf$/i.test(file.name)) {
return ElMessage.warning('仅支持上传 PDF 文件')
}
flowDlg.uploading = true
try {
const fd = new FormData()
fd.append('file', file)
const data = await request.post('/process-flows/upload', fd, {
headers: { 'Content-Type': 'multipart/form-data' }
})
flowDlg.pdfFile = (data && data.filename) || ''
ElMessage.success('PDF 上传成功')
} catch {
// 错误提示已由 request 拦截器统一弹出
} finally {
flowDlg.uploading = false
}
}
const flowDlg = reactive({
show: false, id: 0, name: '', stationNo: 1, pdfFile: '', status: 'ACTIVE',
remark: '', uploading: false, steps: []
})
function openFlow(row) {
if (row) {
flowDlg.id = row.id
flowDlg.name = row.name || ''
flowDlg.stationNo = row.stationNo || 1
flowDlg.pdfFile = row.pdfFile || ''
flowDlg.status = row.status || 'ACTIVE'
flowDlg.remark = row.remark || ''
flowDlg.steps = (row.steps || []).map((s, i) => ({
_key: Date.now() + Math.random(),
seq: s.seq || i + 1,
name: s.name || '',
collectType: s.collectType || 'MANUAL',
isTorque: !!s.isTorque,
remark: s.remark || '',
criteria: (s.criteria || []).map((c) => ({ ...c }))
}))
} else {
Object.assign(flowDlg, {
id: 0, name: '', stationNo: 1, pdfFile: '', status: 'ACTIVE', remark: '', steps: []
})
}
if (!flowDlg.steps.length) flowDlg.steps.push(blankStep(1))
flowDlg.show = true
}
function addStep() {
const maxSeq = flowDlg.steps.reduce((m, s) => Math.max(m, s.seq || 0), 0)
flowDlg.steps.push(blankStep(maxSeq + 1))
}
function removeStep(row) {
const i = flowDlg.steps.indexOf(row)
if (i >= 0) flowDlg.steps.splice(i, 1)
}
/* ---------- 考核标准(独立弹窗) ---------- */
const critDlg = reactive({ show: false, stepName: '', criteria: [], _step: null })
const needsTarget = computed(() => critDlg.criteria.some((c) => c.logic && c.logic !== 'RANGE'))
const needsRange = computed(() => critDlg.criteria.some((c) => c.logic === 'RANGE'))
function openCriteria(step) {
critDlg._step = step
critDlg.stepName = step.name || '(未命名步骤)'
critDlg.criteria = (step.criteria || []).map((c) => ({ ...c }))
critDlg.show = true
}
function addCriterion() {
critDlg.criteria.push({ name: '', unit: '', logic: 'GE', target: null, min: null, max: null })
}
function removeCriterion(c) {
const i = critDlg.criteria.indexOf(c)
if (i >= 0) critDlg.criteria.splice(i, 1)
}
function saveCriteria() {
if (critDlg._step) {
critDlg._step.criteria = critDlg.criteria
.filter((c) => c.name)
.map((c) => ({ ...c }))
}
critDlg.show = false
}
async function saveFlow() {
if (!flowDlg.name) return ElMessage.warning('请填写流程名')
if (!flowDlg.stationNo) return ElMessage.warning('请选择工位号(1~12')
const body = {
id: flowDlg.id || undefined,
name: flowDlg.name,
stationNo: flowDlg.stationNo,
pdfFile: flowDlg.pdfFile,
status: flowDlg.status,
remark: flowDlg.remark,
steps: flowDlg.steps.map(({ _key, ...st }) => ({
seq: st.seq,
name: st.name,
collectType: st.collectType,
isTorque: st.isTorque,
remark: st.remark,
criteria: st.criteria
}))
}
await request.post('/process-flows', body)
ElMessage.success('保存成功')
flowDlg.show = false
loadFlows()
}
async function delFlow(row) {
await ElMessageBox.confirm('确认删除该工艺流程?有工位绑定时将无法删除。', '提示', { type: 'warning' })
await request.delete(`/process-flows/${row.id}`)
ElMessage.success('删除成功')
loadFlows()
}
async function toggleStatus(row) {
const toStatus = row.status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE'
const action = toStatus === 'INACTIVE' ? '停用' : '启用'
if (toStatus === 'INACTIVE') {
await ElMessageBox.confirm(
`确认停用「${row.name}」?停用后将自动解除工位${row.stationNo || ''}的绑定,且该流程不可再被工位使用。`,
'提示', { type: 'warning' })
} else {
await ElMessageBox.confirm(
`确认启用「${row.name}」?启用后将自动绑定到工位${row.stationNo || ''}(该工位若有其他启用流程需先停用)。`,
'提示', { type: 'warning' })
}
await request.post('/process-flows/status', { id: row.id, status: toStatus })
ElMessage.success(`已${action}`)
loadFlows()
}
onMounted(loadFlows)
</script>
<style scoped>
.pdf-name { margin-left: 10px; color: #606266; font-size: 13px; }
</style>