2026-08-31 08:06:55 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<el-card>
|
2026-09-07 11:58:19 +08:00
|
|
|
|
<el-tabs v-model="activeTab">
|
2026-09-22 11:32:29 +08:00
|
|
|
|
<!-- ============ 工艺列表 ============ -->
|
|
|
|
|
|
<el-tab-pane label="工艺列表" name="list">
|
2026-09-07 11:58:19 +08:00
|
|
|
|
<el-form inline>
|
|
|
|
|
|
<el-form-item>
|
|
|
|
|
|
<el-button type="primary" @click="loadFlows">查询</el-button>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item>
|
2026-09-22 11:32:29 +08:00
|
|
|
|
<el-button v-if="can('produce.processflow:add')" type="success" @click="openFlow()">新增工艺</el-button>
|
2026-09-07 11:58:19 +08:00
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-form>
|
2026-08-31 08:06:55 +08:00
|
|
|
|
|
2026-09-07 11:58:19 +08:00
|
|
|
|
<el-table :data="flows" border size="small">
|
|
|
|
|
|
<el-table-column prop="id" label="ID" width="60" />
|
2026-09-22 11:32:29 +08:00
|
|
|
|
<el-table-column prop="name" label="工艺名称" min-width="130" show-overflow-tooltip />
|
2026-09-07 11:58:19 +08:00
|
|
|
|
<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>
|
2026-09-04 14:18:53 +08:00
|
|
|
|
</template>
|
2026-09-07 11:58:19 +08:00
|
|
|
|
</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 }">
|
|
|
|
|
|
<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 }">
|
2026-08-31 08:06:55 +08:00
|
|
|
|
<el-button
|
2026-09-07 11:58:19 +08:00
|
|
|
|
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' ? '停用' : '启用' }}
|
2026-08-31 08:06:55 +08:00
|
|
|
|
</el-button>
|
2026-09-07 11:58:19 +08:00
|
|
|
|
<el-button v-if="can('produce.processflow:delete')" size="small" type="danger" @click="delFlow(row)">删除</el-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
2026-09-08 11:44:04 +08:00
|
|
|
|
<el-pagination style="margin-top:12px;justify-content:flex-end" background layout="total, prev, pager, next" :total="flowTotal" :page-size="flowPageSize" v-model:current-page="flowPage" @current-change="loadFlows" />
|
2026-09-07 11:58:19 +08:00
|
|
|
|
</el-tab-pane>
|
2026-08-31 08:06:55 +08:00
|
|
|
|
|
2026-09-22 11:32:29 +08:00
|
|
|
|
<!-- ============ 工艺配置(新增/编辑共用,替代原弹窗套弹窗) ============ -->
|
|
|
|
|
|
<el-tab-pane label="工艺配置" name="config">
|
2026-09-07 11:58:19 +08:00
|
|
|
|
<el-alert
|
|
|
|
|
|
v-if="!flowDlg.id && !flowDlg.name"
|
|
|
|
|
|
type="info" :closable="false" show-icon
|
2026-09-22 11:32:29 +08:00
|
|
|
|
title="从「工艺列表」点「新增工艺」或某行的「编辑」进入此处;填写后保存即生效。" />
|
2026-09-07 11:58:19 +08:00
|
|
|
|
<el-form :model="flowDlg" label-width="90px" style="margin-top:12px">
|
|
|
|
|
|
<el-row :gutter="12">
|
|
|
|
|
|
<el-col :span="12">
|
2026-09-22 11:32:29 +08:00
|
|
|
|
<el-form-item label="工艺名称" required>
|
|
|
|
|
|
<el-input v-model="flowDlg.name" placeholder="如 3号工位装配工艺" />
|
2026-09-07 11:58:19 +08:00
|
|
|
|
</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>
|
2026-08-31 08:06:55 +08:00
|
|
|
|
|
2026-09-22 11:32:29 +08:00
|
|
|
|
<!-- 工艺步骤子表 -->
|
|
|
|
|
|
<el-form-item label="工艺步骤">
|
2026-09-07 11:58:19 +08:00
|
|
|
|
<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>
|
2026-09-10 16:59:15 +08:00
|
|
|
|
<el-table-column label="检测确认" width="90" align="center">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<el-switch v-model="row.needCheck" />
|
|
|
|
|
|
<el-tooltip content="开启后,报工前须完成该步骤的检测确认" placement="top"><span style="margin-left:4px;color:#909399;font-size:12px">?</span></el-tooltip>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2026-09-07 11:58:19 +08:00
|
|
|
|
<el-table-column label="备注" min-width="120">
|
|
|
|
|
|
<template #default="{ row }"><el-input v-model="row.remark" placeholder="备注(可空)" /></template>
|
|
|
|
|
|
</el-table-column>
|
2026-09-17 12:49:07 +08:00
|
|
|
|
<el-table-column label="图纸/附件" width="190">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<el-upload action="#" :show-file-list="false" multiple
|
|
|
|
|
|
:http-request="(opt) => uploadStepAttachment(opt, row)">
|
|
|
|
|
|
<el-button link type="primary" size="small">上传图纸</el-button>
|
|
|
|
|
|
</el-upload>
|
|
|
|
|
|
<div v-for="(a, ai) in (row.attachment || [])" :key="ai" style="font-size:12px;line-height:1.7">
|
|
|
|
|
|
<el-link type="primary" @click="previewStepFile(a)">{{ a.name }}</el-link>
|
|
|
|
|
|
<el-button link type="danger" size="small" @click="removeStepAttachment(row, ai)">删</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
2026-09-07 11:58:19 +08:00
|
|
|
|
<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>
|
2026-09-22 11:32:29 +08:00
|
|
|
|
|
2026-09-22 14:42:52 +08:00
|
|
|
|
<!-- 物料清单子表(整表覆盖保存;物料编号下拉选择,快照信息取自物料档案) -->
|
2026-09-22 11:32:29 +08:00
|
|
|
|
<el-form-item label="物料清单">
|
|
|
|
|
|
<div style="width:100%">
|
|
|
|
|
|
<el-alert v-if="!flowDlg.id" type="info" :closable="false" style="margin-bottom:8px"
|
|
|
|
|
|
title="新工艺请先保存,保存后即可维护物料清单。" />
|
2026-09-22 14:42:52 +08:00
|
|
|
|
<template v-else>
|
|
|
|
|
|
<el-alert v-if="materialsUnavailable" type="warning" :closable="false" style="margin-bottom:8px"
|
|
|
|
|
|
title="物料档案服务不可用,清单暂只读:不可新增物料行,已有行仅可调整单台用量/损耗率。" />
|
|
|
|
|
|
<el-table :data="flowDlg.materials" border size="small">
|
|
|
|
|
|
<el-table-column label="物料编号" min-width="150">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<el-select v-model="row.materialCode" filterable placeholder="请选择物料编号" style="width:100%"
|
|
|
|
|
|
@change="(code) => onMaterialPicked(row, code)">
|
|
|
|
|
|
<el-option v-for="m in materialOptions" :key="m.code" :label="m.code" :value="m.code" />
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="物料名称" min-width="120">
|
|
|
|
|
|
<template #default="{ row }"><el-input v-model="row.materialName" disabled /></template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="规格" min-width="110">
|
|
|
|
|
|
<template #default="{ row }"><el-input v-model="row.spec" disabled /></template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="单位" width="80">
|
|
|
|
|
|
<template #default="{ row }"><el-input v-model="row.unit" disabled /></template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="管控方式" width="110">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<el-select v-model="row.manageMode" disabled>
|
|
|
|
|
|
<el-option value="1" label="批次" />
|
|
|
|
|
|
<el-option value="2" label="序列号" />
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="单台用量" width="120">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<el-input-number v-model="row.unitQty" :min="0" size="small" controls-position="right" style="width:100%" />
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="损耗率%" width="110">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<el-input-number v-model="row.lossRate" :min="0" size="small" 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="removeMaterial(row)">删</el-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
<div style="display:flex;gap:10px;margin-top:10px;align-items:center">
|
|
|
|
|
|
<el-button v-if="!materialsUnavailable" size="small" @click="addMaterial">新增物料行</el-button>
|
|
|
|
|
|
<el-button size="small" type="primary" :loading="matSaving" @click="saveMaterials">保存物料清单</el-button>
|
|
|
|
|
|
<span style="color:#909399;font-size:12px">物料编号取自物料档案编码(结构件通常即图号),选中后名称/规格/单位/管控方式自动带出、不可改;备料需求 = 各工位分产量 × 单台用量 ×(1+损耗率%)。</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
2026-09-22 11:32:29 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</el-form-item>
|
2026-09-07 11:58:19 +08:00
|
|
|
|
</el-form>
|
|
|
|
|
|
<div style="text-align:right;margin-top:8px">
|
|
|
|
|
|
<el-button @click="activeTab = 'list'">取消</el-button>
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
v-if="can('produce.processflow:add') || can('produce.processflow:edit')"
|
|
|
|
|
|
type="primary" @click="saveFlow">保存</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
</el-tabs>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- ============ 考核标准编辑(单层弹窗,从配置 tab 打开,不再嵌套在弹窗内) ============ -->
|
2026-08-31 12:58:28 +08:00
|
|
|
|
<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>
|
2026-08-31 08:06:55 +08:00
|
|
|
|
<template #footer>
|
2026-08-31 12:58:28 +08:00
|
|
|
|
<el-button @click="critDlg.show = false">取消</el-button>
|
|
|
|
|
|
<el-button type="primary" @click="saveCriteria">确定</el-button>
|
2026-08-31 08:06:55 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</el-dialog>
|
|
|
|
|
|
</el-card>
|
2026-09-07 11:58:19 +08:00
|
|
|
|
<PageHelp :data="helpProcessFlow" page="ProcessFlow" />
|
2026-08-31 08:06:55 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
2026-08-31 12:58:28 +08:00
|
|
|
|
import { ref, reactive, computed, onMounted } from 'vue'
|
2026-08-31 08:06:55 +08:00
|
|
|
|
import PageHelp from '../components/PageHelp.vue'
|
|
|
|
|
|
import { helpProcessFlow } from '../help'
|
|
|
|
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
|
|
|
import request from '../utils/request'
|
|
|
|
|
|
|
2026-09-07 11:58:19 +08:00
|
|
|
|
const activeTab = ref('list')
|
2026-08-31 08:06:55 +08:00
|
|
|
|
const flows = ref([])
|
|
|
|
|
|
const pdfInput = ref(null)
|
2026-09-10 16:59:15 +08:00
|
|
|
|
const flowPage = ref(1)
|
|
|
|
|
|
const flowPageSize = ref(10)
|
|
|
|
|
|
const flowTotal = ref(0)
|
2026-08-31 08:06:55 +08:00
|
|
|
|
|
|
|
|
|
|
/* ---------- 权限 ---------- */
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-31 12:58:28 +08:00
|
|
|
|
function statusText(s) {
|
|
|
|
|
|
return s === 'ACTIVE' ? '启用' : '停用'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-22 11:32:29 +08:00
|
|
|
|
/* ---------- 工艺 ---------- */
|
2026-08-31 08:06:55 +08:00
|
|
|
|
function blankStep(seq) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
_key: Date.now() + Math.random(),
|
|
|
|
|
|
seq,
|
|
|
|
|
|
name: '',
|
|
|
|
|
|
collectType: 'MANUAL',
|
|
|
|
|
|
isTorque: false,
|
2026-09-10 16:59:15 +08:00
|
|
|
|
needCheck: false,
|
2026-08-31 08:06:55 +08:00
|
|
|
|
remark: '',
|
2026-09-17 12:49:07 +08:00
|
|
|
|
attachment: [],
|
2026-08-31 12:58:28 +08:00
|
|
|
|
criteria: []
|
2026-08-31 08:06:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadFlows() {
|
2026-09-08 11:44:04 +08:00
|
|
|
|
const params = { page: flowPage.value, pageSize: flowPageSize.value }
|
|
|
|
|
|
const data = await request.get('/process-flows', { params })
|
|
|
|
|
|
flows.value = (data && data.list) ? data.list : (data || [])
|
|
|
|
|
|
flowTotal.value = (data && data.total) ? data.total : flows.value.length
|
2026-08-31 08:06:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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({
|
2026-09-10 16:59:15 +08:00
|
|
|
|
id: 0, name: '', stations: [], pdfFile: '', status: 'ACTIVE',
|
2026-09-22 11:32:29 +08:00
|
|
|
|
remark: '', uploading: false, steps: [blankStep(1)], materials: []
|
2026-08-31 08:06:55 +08:00
|
|
|
|
})
|
2026-09-22 11:32:29 +08:00
|
|
|
|
const matSaving = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
function blankMaterial() {
|
|
|
|
|
|
return { materialCode: '', materialName: '', spec: '', unit: '', manageMode: '2', unitQty: 1, lossRate: 0 }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-22 14:42:52 +08:00
|
|
|
|
/* ---------- 物料档案(物料清单「物料编号」下拉数据源) ---------- */
|
|
|
|
|
|
const materialOptions = ref([])
|
|
|
|
|
|
const materialsUnavailable = ref(false)
|
|
|
|
|
|
|
|
|
|
|
|
async function loadMaterialArchive() {
|
|
|
|
|
|
materialsUnavailable.value = false
|
|
|
|
|
|
try {
|
|
|
|
|
|
const list = (await request.get('/materials')) || []
|
|
|
|
|
|
materialOptions.value = Array.isArray(list) ? list : []
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
materialOptions.value = []
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!materialOptions.value.length) materialsUnavailable.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 选中物料编号后,快照字段(名称/规格/单位/管控方式)自动带出,均不可手改
|
|
|
|
|
|
function onMaterialPicked(row, code) {
|
|
|
|
|
|
const m = materialOptions.value.find((x) => x.code === code)
|
|
|
|
|
|
if (!m) return
|
|
|
|
|
|
row.materialName = m.name || ''
|
|
|
|
|
|
row.spec = m.spec || ''
|
|
|
|
|
|
row.unit = m.unit || ''
|
|
|
|
|
|
if (m.manageMode != null && m.manageMode !== '') row.manageMode = String(m.manageMode)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-22 11:32:29 +08:00
|
|
|
|
// 加载该工艺的物料清单(整表编辑,保存时整表覆盖)
|
|
|
|
|
|
async function loadMaterials(flowId) {
|
|
|
|
|
|
flowDlg.materials = []
|
|
|
|
|
|
if (!flowId) return
|
|
|
|
|
|
try {
|
|
|
|
|
|
const list = (await request.get(`/process-flows/${flowId}/materials`)) || []
|
|
|
|
|
|
flowDlg.materials = list.map((m) => ({
|
|
|
|
|
|
materialCode: m.materialCode || '', materialName: m.materialName || '', spec: m.spec || '',
|
|
|
|
|
|
unit: m.unit || '', manageMode: m.manageMode || '2', unitQty: m.unitQty ?? 1, lossRate: m.lossRate ?? 0
|
|
|
|
|
|
}))
|
|
|
|
|
|
} catch { /* 拦截器已提示 */ }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function addMaterial() {
|
|
|
|
|
|
flowDlg.materials.push(blankMaterial())
|
|
|
|
|
|
}
|
|
|
|
|
|
function removeMaterial(row) {
|
|
|
|
|
|
const i = flowDlg.materials.indexOf(row)
|
|
|
|
|
|
if (i >= 0) flowDlg.materials.splice(i, 1)
|
|
|
|
|
|
}
|
|
|
|
|
|
async function saveMaterials() {
|
|
|
|
|
|
if (!flowDlg.id) return ElMessage.warning('请先保存工艺,再维护物料清单')
|
|
|
|
|
|
const items = flowDlg.materials.map((m) => ({
|
|
|
|
|
|
materialCode: (m.materialCode || '').trim(),
|
|
|
|
|
|
materialName: (m.materialName || '').trim(),
|
|
|
|
|
|
spec: m.spec || '', unit: m.unit || '',
|
|
|
|
|
|
manageMode: m.manageMode || '2',
|
|
|
|
|
|
unitQty: Number(m.unitQty) || 0,
|
|
|
|
|
|
lossRate: Number(m.lossRate) || 0
|
|
|
|
|
|
}))
|
2026-09-22 14:42:52 +08:00
|
|
|
|
if (items.some((m) => !m.materialCode)) return ElMessage.warning('存在未选择物料编号的行,请补全或删除')
|
2026-09-22 11:32:29 +08:00
|
|
|
|
matSaving.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
await request.post(`/process-flows/${flowDlg.id}/materials`, { items })
|
|
|
|
|
|
ElMessage.success('物料清单已保存')
|
|
|
|
|
|
} catch { /* 拦截器已提示 */ } finally {
|
|
|
|
|
|
matSaving.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-08-31 08:06:55 +08:00
|
|
|
|
|
|
|
|
|
|
function openFlow(row) {
|
|
|
|
|
|
if (row) {
|
|
|
|
|
|
flowDlg.id = row.id
|
|
|
|
|
|
flowDlg.name = row.name || ''
|
2026-09-10 16:59:15 +08:00
|
|
|
|
flowDlg.stations = (row.stations && row.stations.length) ? [...row.stations] : []
|
2026-08-31 08:06:55 +08:00
|
|
|
|
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,
|
2026-09-10 16:59:15 +08:00
|
|
|
|
needCheck: !!s.needCheck,
|
2026-08-31 08:06:55 +08:00
|
|
|
|
remark: s.remark || '',
|
2026-09-17 12:49:07 +08:00
|
|
|
|
attachment: (s.attachment || []).map((a) => ({ ...a })),
|
2026-08-31 12:58:28 +08:00
|
|
|
|
criteria: (s.criteria || []).map((c) => ({ ...c }))
|
2026-08-31 08:06:55 +08:00
|
|
|
|
}))
|
2026-09-22 11:32:29 +08:00
|
|
|
|
loadMaterials(row.id)
|
2026-08-31 08:06:55 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
Object.assign(flowDlg, {
|
2026-09-22 11:32:29 +08:00
|
|
|
|
id: 0, name: '', stations: [], pdfFile: '', status: 'ACTIVE', remark: '', steps: [], materials: []
|
2026-08-31 08:06:55 +08:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!flowDlg.steps.length) flowDlg.steps.push(blankStep(1))
|
2026-09-07 11:58:19 +08:00
|
|
|
|
activeTab.value = 'config'
|
2026-08-31 08:06:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
}
|
2026-08-31 12:58:28 +08:00
|
|
|
|
|
2026-09-17 12:49:07 +08:00
|
|
|
|
/* ---------- 步骤图纸/附件(Item J:图纸绑定工艺步骤,按工序分发到工位终端)---------- */
|
|
|
|
|
|
async function uploadStepAttachment(option, row) {
|
|
|
|
|
|
const fd = new FormData()
|
|
|
|
|
|
fd.append('file', option.file)
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = await request.post('/process-flows/upload', fd, {
|
|
|
|
|
|
headers: { 'Content-Type': 'multipart/form-data' }
|
|
|
|
|
|
})
|
|
|
|
|
|
if (!row.attachment) row.attachment = []
|
|
|
|
|
|
row.attachment.push({ name: option.file.name, url: (data && data.url) || '' })
|
|
|
|
|
|
if (option.onSuccess) option.onSuccess()
|
|
|
|
|
|
ElMessage.success('图纸已上传')
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
if (option.onError) option.onError(e)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
function removeStepAttachment(row, i) {
|
|
|
|
|
|
if (row.attachment) row.attachment.splice(i, 1)
|
|
|
|
|
|
}
|
|
|
|
|
|
function previewStepFile(a) {
|
|
|
|
|
|
if (a && a.url) window.open(a.url, '_blank')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-07 11:58:19 +08:00
|
|
|
|
/* ---------- 考核标准(单层弹窗) ---------- */
|
2026-08-31 12:58:28 +08:00
|
|
|
|
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
|
2026-08-31 08:06:55 +08:00
|
|
|
|
}
|
2026-08-31 12:58:28 +08:00
|
|
|
|
function addCriterion() {
|
|
|
|
|
|
critDlg.criteria.push({ name: '', unit: '', logic: 'GE', target: null, min: null, max: null })
|
2026-08-31 08:06:55 +08:00
|
|
|
|
}
|
2026-08-31 12:58:28 +08:00
|
|
|
|
function removeCriterion(c) {
|
|
|
|
|
|
const i = critDlg.criteria.indexOf(c)
|
|
|
|
|
|
if (i >= 0) critDlg.criteria.splice(i, 1)
|
2026-08-31 08:06:55 +08:00
|
|
|
|
}
|
2026-08-31 12:58:28 +08:00
|
|
|
|
function saveCriteria() {
|
|
|
|
|
|
if (critDlg._step) {
|
|
|
|
|
|
critDlg._step.criteria = critDlg.criteria
|
|
|
|
|
|
.filter((c) => c.name)
|
|
|
|
|
|
.map((c) => ({ ...c }))
|
|
|
|
|
|
}
|
|
|
|
|
|
critDlg.show = false
|
2026-08-31 08:06:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function saveFlow() {
|
2026-09-22 11:32:29 +08:00
|
|
|
|
if (!flowDlg.name) return ElMessage.warning('请填写工艺名称')
|
2026-09-10 16:59:15 +08:00
|
|
|
|
// 绑定关系由「关联工位」页维护:此处只保存流程本身(不收集/不改动工位绑定)
|
2026-08-31 08:06:55 +08:00
|
|
|
|
const body = {
|
|
|
|
|
|
id: flowDlg.id || undefined,
|
|
|
|
|
|
name: flowDlg.name,
|
2026-09-04 14:18:53 +08:00
|
|
|
|
stations: flowDlg.stations,
|
2026-08-31 08:06:55 +08:00
|
|
|
|
pdfFile: flowDlg.pdfFile,
|
|
|
|
|
|
status: flowDlg.status,
|
|
|
|
|
|
remark: flowDlg.remark,
|
2026-08-31 12:58:28 +08:00
|
|
|
|
steps: flowDlg.steps.map(({ _key, ...st }) => ({
|
2026-08-31 08:06:55 +08:00
|
|
|
|
seq: st.seq,
|
|
|
|
|
|
name: st.name,
|
|
|
|
|
|
collectType: st.collectType,
|
|
|
|
|
|
isTorque: st.isTorque,
|
2026-09-10 16:59:15 +08:00
|
|
|
|
needCheck: st.needCheck,
|
2026-08-31 08:06:55 +08:00
|
|
|
|
remark: st.remark,
|
2026-09-17 12:49:07 +08:00
|
|
|
|
attachment: st.attachment || [],
|
2026-08-31 08:06:55 +08:00
|
|
|
|
criteria: st.criteria
|
|
|
|
|
|
}))
|
|
|
|
|
|
}
|
|
|
|
|
|
await request.post('/process-flows', body)
|
|
|
|
|
|
ElMessage.success('保存成功')
|
2026-09-07 11:58:19 +08:00
|
|
|
|
activeTab.value = 'list'
|
2026-08-31 08:06:55 +08:00
|
|
|
|
loadFlows()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function delFlow(row) {
|
2026-09-22 11:32:29 +08:00
|
|
|
|
await ElMessageBox.confirm('确认删除该工艺?删除将同时解除其绑定的工位。', '提示', { type: 'warning' })
|
2026-08-31 08:06:55 +08:00
|
|
|
|
await request.delete(`/process-flows/${row.id}`)
|
|
|
|
|
|
ElMessage.success('删除成功')
|
|
|
|
|
|
loadFlows()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-31 12:58:28 +08:00
|
|
|
|
async function toggleStatus(row) {
|
|
|
|
|
|
const toStatus = row.status === 'ACTIVE' ? 'INACTIVE' : 'ACTIVE'
|
|
|
|
|
|
const action = toStatus === 'INACTIVE' ? '停用' : '启用'
|
|
|
|
|
|
if (toStatus === 'INACTIVE') {
|
|
|
|
|
|
await ElMessageBox.confirm(
|
2026-09-22 11:32:29 +08:00
|
|
|
|
`确认停用「${row.name}」?停用后绑定该工艺的工位将不再加载其作业步骤(绑定关系保留,启用后自动恢复)。`,
|
2026-08-31 12:58:28 +08:00
|
|
|
|
'提示', { type: 'warning' })
|
2026-08-31 08:06:55 +08:00
|
|
|
|
} else {
|
2026-08-31 12:58:28 +08:00
|
|
|
|
await ElMessageBox.confirm(
|
2026-09-22 11:32:29 +08:00
|
|
|
|
`确认启用「${row.name}」?启用后其绑定工位(${(row.stations || []).map(s => '工位' + s).join('、') || '-'})将恢复使用该工艺。`,
|
2026-09-07 11:58:19 +08:00
|
|
|
|
'提示', { type: 'warning' }
|
|
|
|
|
|
)
|
2026-08-31 08:06:55 +08:00
|
|
|
|
}
|
2026-08-31 12:58:28 +08:00
|
|
|
|
await request.post('/process-flows/status', { id: row.id, status: toStatus })
|
|
|
|
|
|
ElMessage.success(`已${action}`)
|
|
|
|
|
|
loadFlows()
|
2026-08-31 08:06:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-10 16:59:15 +08:00
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
loadFlows()
|
2026-09-22 14:42:52 +08:00
|
|
|
|
loadMaterialArchive()
|
2026-09-10 16:59:15 +08:00
|
|
|
|
})
|
2026-08-31 08:06:55 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.pdf-name { margin-left: 10px; color: #606266; font-size: 13px; }
|
|
|
|
|
|
</style>
|