feat: 新增装机绑定功能与权限、BOM工序配置
1. 新增装机绑定实体与相关CRUD逻辑,支持工件工序物料绑定/解绑 2. 为BOM物料新增装配工序字段,支持按工序校验绑定 3. 扩展权限表,新增父权限码字段支持菜单按钮关联 4. 统一各页面帮助弹窗参数,新增角色管理帮助文档 5. 新增公共格式化工具函数,优化侧边栏菜单展开逻辑 6. 新增工位终端绑定代理接口与MES内部绑定API 7. 修复主入口数据库连接与schema初始化逻辑,新增一键迁移工具
This commit is contained in:
@@ -1,8 +1,6 @@
|
||||
<template>
|
||||
<!-- 页面帮助抽屉:由右上角「帮助」按钮或点击页面标题触发(mes:open-page-help 事件) -->
|
||||
<div class="page-help">
|
||||
<el-drawer v-model="show" :title="data.title || '操作说明'" size="520px" append-to-body class="ph-drawer">
|
||||
<!-- 顶部概览 -->
|
||||
<!-- 帮助内容通过右上角「帮助」按钮触发(MainLayout 派发 mes:open-page-help 事件) -->
|
||||
<el-drawer v-model="show" :title="data.title || '操作说明'" size="520px" append-to-body class="ph-drawer">
|
||||
<div class="ph-inner">
|
||||
<div class="ph-banner">
|
||||
<div class="ph-banner-title">{{ data.title || '操作说明' }}</div>
|
||||
@@ -47,22 +45,30 @@
|
||||
|
||||
<!-- 既无顶层字段也无分组 -->
|
||||
<el-empty v-if="!data.fields && !(data.sections && data.sections.length)"
|
||||
description="暂无说明" :image-size="70" />
|
||||
description="该页面暂未编写操作说明" :image-size="70" />
|
||||
</div>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
defineProps({
|
||||
data: { type: Object, default: () => ({ title: '操作说明', overview: '', fields: [] }) }
|
||||
const props = defineProps({
|
||||
data: { type: Object, default: () => ({ title: '操作说明', overview: '', fields: [] }) },
|
||||
// 归属路由名(route.name):仅当其为当前激活页时才弹出,避免 keep-alive 下多页抽屉同时弹出
|
||||
page: { type: String, default: '' }
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const show = ref(false)
|
||||
function open() { show.value = true }
|
||||
function open() {
|
||||
// 未声明归属页 → 始终打开;声明了则必须与当前路由一致才打开(保证只弹自己)
|
||||
if (props.page && route.name !== props.page) return
|
||||
show.value = true
|
||||
}
|
||||
|
||||
// 右上角「帮助」按钮(MainLayout)通过 mes:open-page-help 事件触发当前页帮助抽屉
|
||||
onMounted(() => window.addEventListener('mes:open-page-help', open))
|
||||
onBeforeUnmount(() => window.removeEventListener('mes:open-page-help', open))
|
||||
</script>
|
||||
|
||||
@@ -24,16 +24,19 @@ export const helpWorkOrder = {
|
||||
title: '工单管理(新建 / 编辑 / 删除工单)',
|
||||
overview:
|
||||
'工单 = 一个"合同/生产批次"的唯一载体,是生产执行和仓库出库挂靠的核心对象。\n\n全流程:先有产品类型 → 建工单(选产品类型、定数量、勾选工位组合) → 进"物料清单"给该产品配料 → 到"日排产"排哪天产多少 → 仓库按排产备料出库 → 产线装配/拧紧/报工 → 完工。\n工单号会贯穿 备料单、拧紧查询、手动报工、工件追溯、操作日志 全链路。\n\n查询:支持按 工单号 / 产品编码 / 产品名称(模糊) / 状态 组合筛选。\n删除:仅「已创建 / 待排产 / 已发布」且无排产、备料、生产数据的工单可删除(操作列「删除」),生产中或已完成的工单不可删除。',
|
||||
fields: [
|
||||
{ name: '工单号', source: '系统自动生成(WO+时间戳),可手工改。新建时自动带出。', purpose: '唯一标识一个工单,备料/出库/报工/追溯都挂在此号下', fill: '一般不用改;如改动须保证唯一' },
|
||||
{ name: '产品类型', source: '下拉选择。数据来自"产品类型"页面已启用的记录。', purpose: '说明这批生产的是什么产品,并决定物料清单内容', fill: '先到"产品类型"页面建好记录再选择' },
|
||||
{ name: '产品编码', source: '手工填写。通常与所选产品类型的编码一致。', purpose: '产品唯一编码,用于展示与追溯', fill: '如 METER-01,与产品类型编码对齐' },
|
||||
{ name: '产品名称', source: '手工填写。通常与产品类型名称一致。', purpose: '产品中文名,用于展示', fill: '如 智能电表' },
|
||||
{ name: '数量', source: '手工填写整数。', purpose: '本工单生产总数量(合同台数),决定需求量与排产上限', fill: '整数 ≥1,如 100' },
|
||||
{ name: '工位组合', source: '勾选 12 个工位(工位1~工位12)。', purpose: '本工单要经过的工位集合,PLC 按此组合下发控制产线', fill: '勾选要经过的工位即可,系统自动按 1→12 顺序执行(传送带不回走);半成品再上线可只勾漏做的工位' },
|
||||
{ name: '状态', source: '新建时选"已创建",后续由流程推进。', purpose: '工单生命周期:创建→发布→生产→完成', fill: '新建时选"已创建";也可在列表操作列点「状态」快捷流转(发布/开工/完工)' }
|
||||
],
|
||||
sections: [
|
||||
{
|
||||
title: '工单基本信息',
|
||||
fields: [
|
||||
{ name: '工单号', source: '系统自动生成(WO+时间戳),可手工改。新建时自动带出。', purpose: '唯一标识一个工单,备料/出库/报工/追溯都挂在此号下', fill: '一般不用改;如改动须保证唯一' },
|
||||
{ name: '产品类型', source: '下拉选择。数据来自"产品类型"页面已启用的记录。', purpose: '说明这批生产的是什么产品,并决定物料清单内容', fill: '先到"产品类型"页面建好记录再选择' },
|
||||
{ name: '产品编码', source: '手工填写。通常与所选产品类型的编码一致。', purpose: '产品唯一编码,用于展示与追溯', fill: '如 METER-01,与产品类型编码对齐' },
|
||||
{ name: '产品名称', source: '手工填写。通常与产品类型名称一致。', purpose: '产品中文名,用于展示', fill: '如 智能电表' },
|
||||
{ name: '数量', source: '手工填写整数。', purpose: '本工单生产总数量(合同台数),决定需求量与排产上限', fill: '整数 ≥1,如 100' },
|
||||
{ name: '工位组合', source: '勾选 12 个工位(工位1~工位12)。', purpose: '本工单要经过的工位集合,PLC 按此组合下发控制产线', fill: '勾选要经过的工位即可,系统自动按 1→12 顺序执行(传送带不回走);半成品再上线可只勾漏做的工位' },
|
||||
{ name: '状态', source: '新建时选"已创建",后续由流程推进。', purpose: '工单生命周期:创建→发布→生产→完成', fill: '新建时选"已创建";也可在列表操作列点「状态」快捷流转(发布/开工/完工)' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '行内"工位组合"维护',
|
||||
overview: '对当前工单快速维护要经过的工位组合。\n可自由勾选、补工位(半成品再上线):如本应走第3/5工位却做成半成品,可在此只勾第2工位重上线完成闭环。',
|
||||
@@ -215,15 +218,18 @@ export const helpEventLog = {
|
||||
export const helpProcessFlow = {
|
||||
title: '工艺流程(服务于固定工位,含工序步骤)',
|
||||
overview:
|
||||
'工艺流程 = 某个固定工位(1~12)的作业指导:包含 工位号、工艺图纸PDF、工序步骤(含采集方式与考核标准)。\n\n概念统一说明:\n· 工位:产线固定 12 个物理工位(工位1~工位12),本流程页选择流程服务于哪个工位。\n· 工艺流程:一个流程服务于一个工位,可有多版本(如 3号工位_2026版);一个工位只能有一个「启用」的流程。\n· 工序步骤:流程内包含多个工序步骤(如:装配完成、拧紧扭矩、测试)。\n\n绑定规则:流程选工位号并「启用」后,自动绑定到该工位;「停用」后自动解绑。手动报工页选工位时,自动带出该工位启用流程的工序步骤。\n\n数据链路:本页维护流程+工序步骤+考核标准 → 自动绑定工位 → 手动报工/工位终端按工序步骤采集 → 报工按考核标准自动判 OK/NG → 工件追溯/绩效报表可见。',
|
||||
fields: [
|
||||
{ name: '流程名称', source: '手工填写。人为定义。', purpose: '流程版本名,用于区分同一工位的不同版本', fill: '如 3号工位装配流程 / 3号工位_2026版' },
|
||||
{ name: '工位号', source: '下拉选择(1~12)。产线固定 12 个工位。', purpose: '该流程服务于哪个工位;一个工位只能有一个启用流程', fill: '如 3;该工位已有启用流程时需先停用再启用新流程' },
|
||||
{ name: '状态', source: '下拉选择 启用/停用。也可在列表操作列点「启用/停用」快捷切换。', purpose: '启用=自动绑定该工位并参与生产;停用=自动解绑工位', fill: '启用后自动绑定工位;停用后自动解绑' },
|
||||
{ name: '工艺图纸(PDF)', source: '本页上传。文件由 MES 服务器保存。', purpose: '该工位的作业指导图,工位终端打开预览', fill: '选择 PDF 上传,成功后显示文件名可预览' },
|
||||
{ name: '工序步骤', source: '本页维护(步骤名/采集方式/是否拧紧/考核标准)。', purpose: '手动报工/工位终端报工时按此动态渲染参数输入', fill: '一个流程可含多个工序步骤,按序号顺序执行' }
|
||||
],
|
||||
'页面分「流程列表」「流程配置」两个页签:列表查看全部流程并做启用/停用/删除;配置页维护流程基本信息与工序步骤(新增与编辑共用,不再弹窗套弹窗)。\n\n工艺流程 = 某个固定工位(1~12)的作业指导:包含 工位号、工艺图纸PDF、工序步骤(含采集方式与考核标准)。\n\n概念统一说明:\n· 工位:产线固定 12 个物理工位(工位1~工位12),本流程页选择流程服务于哪个工位。\n· 工艺流程:一个流程服务于一个工位,可有多版本(如 3号工位_2026版);一个工位只能有一个「启用」的流程。\n· 工序步骤:流程内包含多个工序步骤(如:装配完成、拧紧扭矩、测试)。\n\n绑定规则:流程选工位号并「启用」后,自动绑定到该工位;「停用」后自动解绑。手动报工页选工位时,自动带出该工位启用流程的工序步骤。\n\n数据链路:本页维护流程+工序步骤+考核标准 → 自动绑定工位 → 手动报工/工位终端按工序步骤采集 → 报工按考核标准自动判 OK/NG → 工件追溯/绩效报表可见。',
|
||||
sections: [
|
||||
{
|
||||
title: '流程基本信息',
|
||||
fields: [
|
||||
{ name: '流程名称', source: '手工填写。人为定义。', purpose: '流程版本名,用于区分同一工位的不同版本', fill: '如 3号工位装配流程 / 3号工位_2026版' },
|
||||
{ name: '工位号', source: '下拉选择(1~12)。产线固定 12 个工位。', purpose: '该流程服务于哪个工位;一个工位只能有一个启用流程', fill: '如 3;该工位已有启用流程时需先停用再启用新流程' },
|
||||
{ name: '状态', source: '下拉选择 启用/停用。也可在列表操作列点「启用/停用」快捷切换。', purpose: '启用=自动绑定该工位并参与生产;停用=自动解绑工位', fill: '启用后自动绑定工位;停用后自动解绑' },
|
||||
{ name: '工艺图纸(PDF)', source: '本页上传。文件由 MES 服务器保存。', purpose: '该工位的作业指导图,工位终端打开预览', fill: '选择 PDF 上传,成功后显示文件名可预览' },
|
||||
{ name: '工序步骤', source: '本页维护(步骤名/采集方式/是否拧紧/考核标准)。', purpose: '手动报工/工位终端报工时按此动态渲染参数输入', fill: '一个流程可含多个工序步骤,按序号顺序执行' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '采集方式(每个工序步骤三选一)',
|
||||
overview: '决定该步骤的数值从哪里来。',
|
||||
@@ -249,7 +255,7 @@ export const helpProcessFlow = {
|
||||
export const helpPerformance = {
|
||||
title: '绩效报表(工人工作量统计)',
|
||||
overview:
|
||||
'按 操作人 × 日期 × 工位 汇总每个工人的完成/合格/NG 数量,用于工作量与绩效统计。\n\n数据来源:全部由工位终端/手动报工的「工位完成上报」自动写入报工实绩(workpiece_process),本页只读统计,不手工录入。\n\n数据链路:工位终端报工(写报工实绩) → 本页按人/天/工位聚合 → 出报表。',
|
||||
'按 操作人 × 日期 × 工位 汇总每个工人的完成/合格/NG 数量,用于工作量与绩效统计。\n\n页面提供「按人 / 按工位 / 明细」三个视图切换:按人看每人汇总,按工位看每工位汇总,明细看逐条记录;三者均受上方筛选条件约束,顶部汇总卡为当前筛选的总计。\n\n数据来源:全部由工位终端/手动报工的「工位完成上报」自动写入报工实绩(workpiece_process),本页只读统计,不手工录入。\n\n数据链路:工位终端报工(写报工实绩) → 本页按人/天/工位聚合 → 出报表。',
|
||||
fields: [
|
||||
{ name: '操作人', source: '筛选条件。来自报工实绩的 operator 字段(工位终端登录人)。', purpose: '只看某个工人的工作量', fill: '输入姓名/账号,可空=全部' },
|
||||
{ name: '日期范围', source: '筛选条件。来自报工实绩的产生时间。', purpose: '按时间段统计', fill: '选择起止日期,可空=全部' },
|
||||
|
||||
@@ -71,7 +71,7 @@ import request from '../utils/request'
|
||||
const router = useRouter()
|
||||
|
||||
// ===== 响应式侧边栏:桌面可折叠,移动端抽屉 =====
|
||||
const isCollapse = ref(false) // 桌面端折叠(仅图标)
|
||||
const isCollapse = ref(false) // 桌面端默认展开(汉堡菜单展开态);基础配置/系统管理分组靠 defaultOpeneds 默认收起
|
||||
const isMobile = ref(false) // 移动端(≤767px)
|
||||
const mobileOpen = ref(false) // 移动端抽屉展开
|
||||
|
||||
@@ -149,10 +149,17 @@ const menuGroups = [
|
||||
}
|
||||
]
|
||||
|
||||
// 默认展开全部分组;折叠→展开时重建菜单使 default-openeds 重新生效
|
||||
// 默认展开分组:仅展开高频业务组(生产管理/现场作业);
|
||||
// 基础配置(用户称"集成配置")、系统管理 默认收起,需手动点开。
|
||||
// 折叠→展开时重建菜单使 default-openeds 重新生效
|
||||
// (Element Plus 折叠会清空内部 openedMenus,default-openeds 仅在初始化时读取一次,
|
||||
// 故用 key 变化强制重建实例 —— 同 WMS 的处理)
|
||||
const defaultOpeneds = computed(() => menus.value.map((g) => 'group:' + g.title))
|
||||
const collapsedGroups = ['基础配置', '系统管理']
|
||||
const defaultOpeneds = computed(() =>
|
||||
menus.value
|
||||
.map((g) => 'group:' + g.title)
|
||||
.filter((k) => !collapsedGroups.includes(k.replace('group:', '')))
|
||||
)
|
||||
const menuKey = ref(0)
|
||||
watch(isCollapse, (collapsed) => {
|
||||
if (!collapsed) menuKey.value++
|
||||
|
||||
@@ -21,6 +21,18 @@
|
||||
<el-table-column prop="manageMode" label="管理方式" width="110">
|
||||
<template #default="{row}">{{ row.manageMode === '1' ? '结构件(批次)' : '精密件(SN)' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="processCode" label="装配工序" width="180">
|
||||
<template #default="{row}">
|
||||
<template v-if="can('produce.bom:edit')">
|
||||
<el-select v-model="row.processCode" size="small" style="width:150px" @change="setProcess(row)">
|
||||
<el-option :value="0" label="未指定(不校验)" />
|
||||
<el-option v-for="i in 12" :key="i" :value="i" :label="`工位${i}`" />
|
||||
</el-select>
|
||||
</template>
|
||||
<el-tag v-else-if="row.processCode > 0" size="small" type="primary">工位{{ row.processCode }}</el-tag>
|
||||
<span v-else style="color:#909399">未指定</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="unitQty" label="单台用量" width="90" />
|
||||
<el-table-column prop="lossRate" label="损耗率%" width="90" />
|
||||
<el-table-column label="操作" width="90">
|
||||
@@ -49,6 +61,12 @@
|
||||
<el-radio label="1">结构件(批次)</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="装配工序">
|
||||
<el-select v-model="add.processCode" clearable placeholder="未指定=不参与工位绑定校验" style="width:100%">
|
||||
<el-option v-for="i in 12" :key="i" :label="`工位${i}`" :value="i" />
|
||||
</el-select>
|
||||
<div style="color:#909399;font-size:12px;line-height:1.4;width:100%">该料在第几道工序被装配。指定后,此工序报工/完工时强制校验绑定齐套:精密件按单台用量逐颗扫SN、结构件至少绑一个批次号。</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="单台用量"><el-input-number v-model="add.unitQty" :min="0" /></el-form-item>
|
||||
<el-form-item label="损耗率%"><el-input-number v-model="add.lossRate" :min="0" /></el-form-item>
|
||||
<el-form-item label="需求量">
|
||||
@@ -62,7 +80,7 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
<PageHelp :data="helpBom" />
|
||||
<PageHelp :data="helpBom" page="Bom" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -76,7 +94,7 @@ import { helpBom } from '../help'
|
||||
const productCode = ref('')
|
||||
const products = ref([])
|
||||
const list = ref([])
|
||||
const add = reactive({ show: false, productCode: '', materialCode: '', materialName: '', spec: '', unit: '', manageMode: '2', unitQty: 1, lossRate: 0 })
|
||||
const add = reactive({ show: false, productCode: '', materialCode: '', materialName: '', spec: '', unit: '', manageMode: '2', processCode: 0, unitQty: 1, lossRate: 0 })
|
||||
// 需求量仅展示用(公式:单台用量×计划数量×(1+损耗率%)),备料生成时按工单计划数量实时计算
|
||||
const autoReqQty = computed(() => {
|
||||
if (!add.unitQty) return '0'
|
||||
@@ -88,12 +106,13 @@ async function load() {
|
||||
list.value = (await request.get('/bom', { params: { productCode: productCode.value } })) || []
|
||||
}
|
||||
function openAdd() {
|
||||
Object.assign(add, { show: true, productCode: productCode.value || '', materialCode: '', materialName: '', spec: '', unit: '', manageMode: '2', unitQty: 1, lossRate: 0 })
|
||||
Object.assign(add, { show: true, productCode: productCode.value || '', materialCode: '', materialName: '', spec: '', unit: '', manageMode: '2', processCode: 0, unitQty: 1, lossRate: 0 })
|
||||
}
|
||||
async function save() {
|
||||
if (!add.productCode) return ElMessage.warning('请选择产品编码')
|
||||
if (!add.materialCode) return ElMessage.warning('请填写物料编码')
|
||||
const { requiredQty, ...item } = add
|
||||
if (!item.processCode) item.processCode = 0
|
||||
await request.put('/bom', { productCode: add.productCode, items: [{ ...item }] })
|
||||
ElMessage.success('保存成功')
|
||||
add.show = false
|
||||
@@ -104,10 +123,18 @@ async function remove(row) {
|
||||
await ElMessageBox.confirm('确认移除该物料?', '提示', { type: 'warning' })
|
||||
const items = list.value
|
||||
.filter((x) => x.id !== row.id)
|
||||
.map(({ materialCode, materialName, spec, unit, manageMode, unitQty, lossRate }) => ({ materialCode, materialName, spec, unit, manageMode, unitQty, lossRate }))
|
||||
.map(({ materialCode, materialName, spec, unit, manageMode, processCode, unitQty, lossRate }) => ({ materialCode, materialName, spec, unit, manageMode, processCode: processCode || 0, unitQty, lossRate }))
|
||||
await request.put('/bom', { productCode: productCode.value, items })
|
||||
await load()
|
||||
}
|
||||
// 行内修改装配工序:单行保存
|
||||
async function setProcess(row) {
|
||||
const { processCode } = row
|
||||
const { materialCode, materialName, spec, unit, manageMode, unitQty, lossRate } = row
|
||||
await request.put('/bom', { productCode: row.productCode || productCode.value, items: [{ materialCode, materialName, spec, unit, manageMode, processCode: processCode || 0, unitQty, lossRate }] })
|
||||
ElMessage.success('已更新装配工序')
|
||||
await load()
|
||||
}
|
||||
onMounted(async () => {
|
||||
products.value = (await request.get('/product-types')) || []
|
||||
})
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
<PageHelp :data="helpDailyPlan" />
|
||||
<PageHelp :data="helpDailyPlan" page="DailyPlan" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -172,7 +172,7 @@
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
<PageHelp :data="helpInspect" />
|
||||
<PageHelp :data="helpInspect" page="Inspect" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<el-table-column prop="operator" label="操作人" width="90" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
<PageHelp :data="helpMaterial" />
|
||||
<PageHelp :data="helpMaterial" page="MaterialRequest" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 汇总卡 -->
|
||||
<!-- 汇总卡(基于当前筛选的全部明细,三个视图共用) -->
|
||||
<el-row :gutter="16" style="margin-bottom:14px">
|
||||
<el-col :span="6">
|
||||
<el-card shadow="never" class="stat-card">
|
||||
@@ -50,24 +50,64 @@
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-table :data="rows" border size="small">
|
||||
<el-table-column prop="operator" label="操作人" min-width="110" />
|
||||
<el-table-column prop="date" label="日期" width="110" />
|
||||
<el-table-column prop="processCode" label="工位" width="80" align="center" />
|
||||
<el-table-column prop="processName" label="工位名" min-width="130" show-overflow-tooltip />
|
||||
<el-table-column prop="doneCount" label="完成数" width="90" align="right" />
|
||||
<el-table-column prop="okCount" label="合格数" width="90" align="right" />
|
||||
<el-table-column prop="ngCount" label="NG数" width="90" align="right" />
|
||||
<el-table-column label="合格率" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="rateOf(row) >= 100 ? 'success' : (rateOf(row) >= 0 ? 'warning' : 'info')" effect="plain">
|
||||
{{ rateText(row) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-tabs v-model="activeTab">
|
||||
<!-- 按人 -->
|
||||
<el-tab-pane label="按人" name="byOperator">
|
||||
<el-table :data="rowsByOperator" border size="small">
|
||||
<el-table-column prop="operator" label="操作人" min-width="130" />
|
||||
<el-table-column prop="doneCount" label="完成数" width="110" align="right" />
|
||||
<el-table-column prop="okCount" label="合格数" width="110" align="right" />
|
||||
<el-table-column prop="ngCount" label="NG数" width="110" align="right" />
|
||||
<el-table-column label="合格率" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="rateOf(row) >= 100 ? 'success' : (rateOf(row) >= 0 ? 'warning' : 'info')" effect="plain">
|
||||
{{ rateText(row) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 按工位 -->
|
||||
<el-tab-pane label="按工位" name="byStation">
|
||||
<el-table :data="rowsByStation" border size="small">
|
||||
<el-table-column prop="station" label="工位" width="90" align="center" />
|
||||
<el-table-column prop="stationName" label="工位名" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="doneCount" label="完成数" width="110" align="right" />
|
||||
<el-table-column prop="okCount" label="合格数" width="110" align="right" />
|
||||
<el-table-column prop="ngCount" label="NG数" width="110" align="right" />
|
||||
<el-table-column label="合格率" width="110" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="rateOf(row) >= 100 ? 'success' : (rateOf(row) >= 0 ? 'warning' : 'info')" effect="plain">
|
||||
{{ rateText(row) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 明细 -->
|
||||
<el-tab-pane label="明细" name="detail">
|
||||
<el-table :data="rows" border size="small">
|
||||
<el-table-column prop="operator" label="操作人" min-width="110" />
|
||||
<el-table-column prop="date" label="日期" width="110" />
|
||||
<el-table-column prop="processCode" label="工位" width="80" align="center" />
|
||||
<el-table-column prop="processName" label="工位名" min-width="130" show-overflow-tooltip />
|
||||
<el-table-column prop="doneCount" label="完成数" width="90" align="right" />
|
||||
<el-table-column prop="okCount" label="合格数" width="90" align="right" />
|
||||
<el-table-column prop="ngCount" label="NG数" width="90" align="right" />
|
||||
<el-table-column label="合格率" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="rateOf(row) >= 100 ? 'success' : (rateOf(row) >= 0 ? 'warning' : 'info')" effect="plain">
|
||||
{{ rateText(row) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
<PageHelp :data="helpPerformance" />
|
||||
<PageHelp :data="helpPerformance" page="Performance" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -77,6 +117,7 @@ import PageHelp from '../components/PageHelp.vue'
|
||||
import { helpPerformance } from '../help'
|
||||
import request from '../utils/request'
|
||||
|
||||
const activeTab = ref('byOperator')
|
||||
const query = reactive({ operator: '', range: null })
|
||||
const rows = ref([])
|
||||
|
||||
@@ -93,6 +134,34 @@ const summary = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// 按人聚合:同一操作人的完成/合格/NG 累加
|
||||
const rowsByOperator = computed(() => {
|
||||
const m = new Map()
|
||||
for (const r of rows.value) {
|
||||
const k = r.operator
|
||||
if (!m.has(k)) m.set(k, { operator: k, doneCount: 0, okCount: 0, ngCount: 0 })
|
||||
const o = m.get(k)
|
||||
o.doneCount += Number(r.doneCount) || 0
|
||||
o.okCount += Number(r.okCount) || 0
|
||||
o.ngCount += Number(r.ngCount) || 0
|
||||
}
|
||||
return [...m.values()]
|
||||
})
|
||||
|
||||
// 按工位聚合:同一工位(工序编号)的完成/合格/NG 累加
|
||||
const rowsByStation = computed(() => {
|
||||
const m = new Map()
|
||||
for (const r of rows.value) {
|
||||
const k = r.processCode
|
||||
if (!m.has(k)) m.set(k, { station: r.processCode, stationName: r.processName, doneCount: 0, okCount: 0, ngCount: 0 })
|
||||
const o = m.get(k)
|
||||
o.doneCount += Number(r.doneCount) || 0
|
||||
o.okCount += Number(r.okCount) || 0
|
||||
o.ngCount += Number(r.ngCount) || 0
|
||||
}
|
||||
return [...m.values()]
|
||||
})
|
||||
|
||||
function rateOf(row) {
|
||||
const done = Number(row.doneCount) || 0
|
||||
const ok = Number(row.okCount) || 0
|
||||
@@ -114,20 +183,29 @@ async function load() {
|
||||
rows.value = data.rows || []
|
||||
}
|
||||
|
||||
// 导出 CSV(块6):按当前查询结果生成,含 BOM 以便 Excel 打开中文不乱码
|
||||
// 导出 CSV(块6):按当前激活的 tab 视图生成,含 BOM 以便 Excel 打开中文不乱码
|
||||
function exportCsv() {
|
||||
if (!rows.value.length) return ElMessage.warning('当前无数据可导出')
|
||||
const header = ['操作人', '日期', '工位', '工位名', '完成数', '合格数', 'NG数', '合格率']
|
||||
const lines = rows.value.map((r) => {
|
||||
const rate = Number(r.doneCount) ? ((Number(r.okCount) / Number(r.doneCount)) * 100).toFixed(1) + '%' : '-'
|
||||
return [r.operator, r.date, r.processCode, r.processName, r.doneCount, r.okCount, r.ngCount, rate]
|
||||
})
|
||||
let header, lines
|
||||
if (activeTab.value === 'byOperator') {
|
||||
header = ['操作人', '完成数', '合格数', 'NG数', '合格率']
|
||||
lines = rowsByOperator.value.map((r) => [r.operator, r.doneCount, r.okCount, r.ngCount, rateText(r)])
|
||||
} else if (activeTab.value === 'byStation') {
|
||||
header = ['工位', '工位名', '完成数', '合格数', 'NG数', '合格率']
|
||||
lines = rowsByStation.value.map((r) => [r.station, r.stationName, r.doneCount, r.okCount, r.ngCount, rateText(r)])
|
||||
} else {
|
||||
header = ['操作人', '日期', '工位', '工位名', '完成数', '合格数', 'NG数', '合格率']
|
||||
lines = rows.value.map((r) => {
|
||||
const rate = Number(r.doneCount) ? ((Number(r.okCount) / Number(r.doneCount)) * 100).toFixed(1) + '%' : '-'
|
||||
return [r.operator, r.date, r.processCode, r.processName, r.doneCount, r.okCount, r.ngCount, rate]
|
||||
})
|
||||
}
|
||||
if (!lines.length) return ElMessage.warning('当前无数据可导出')
|
||||
const esc = (v) => `"${String(v ?? '').replace(/"/g, '""')}"`
|
||||
const csv = '\ufeff' + [header, ...lines].map((row) => row.map(esc).join(',')).join('\r\n')
|
||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' })
|
||||
const a = document.createElement('a')
|
||||
a.href = URL.createObjectURL(blob)
|
||||
a.download = `绩效报表_${new Date().toISOString().slice(0, 10)}.csv`
|
||||
a.download = `绩效报表_${activeTab.value}_${new Date().toISOString().slice(0, 10)}.csv`
|
||||
a.click()
|
||||
URL.revokeObjectURL(a.href)
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
<el-table-column prop="operator" label="操作人" width="90" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
<PageHelp :data="helpPlc" />
|
||||
<PageHelp :data="helpPlc" page="PlcSend" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
@@ -1,178 +1,187 @@
|
||||
<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-tabs v-model="activeTab">
|
||||
<!-- ============ 流程列表 ============ -->
|
||||
<el-tab-pane label="流程列表" name="list">
|
||||
<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="100" 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="150">
|
||||
<template #default="{ row }">
|
||||
<el-popover v-if="(row.stations || []).length" placement="top" :width="220" trigger="hover">
|
||||
<template #reference>
|
||||
<el-tag size="small" type="success">{{ (row.stations || []).length }} 个工位</el-tag>
|
||||
<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="100" 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>
|
||||
<div style="line-height:1.9">{{ (row.stations || []).map(s => `工位${s}`).join('、') }}</div>
|
||||
</el-popover>
|
||||
<span v-else>-</span>
|
||||
</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-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="150">
|
||||
<template #default="{ row }">
|
||||
<el-popover v-if="(row.stations || []).length" placement="top" :width="220" trigger="hover">
|
||||
<template #reference>
|
||||
<el-tag size="small" type="success">{{ (row.stations || []).length }} 个工位</el-tag>
|
||||
</template>
|
||||
<div style="line-height:1.9">{{ (row.stations || []).map(s => `工位${s}`).join('、') }}</div>
|
||||
</el-popover>
|
||||
<span v-else>-</span>
|
||||
</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-tab-pane>
|
||||
|
||||
<!-- ============ 流程配置(新增/编辑共用,替代原弹窗套弹窗) ============ -->
|
||||
<el-tab-pane label="流程配置" name="config">
|
||||
<el-alert
|
||||
v-if="!flowDlg.id && !flowDlg.name"
|
||||
type="info" :closable="false" show-icon
|
||||
title="从「流程列表」点「新增流程」或某行的「编辑」进入此处;填写后保存即生效。" />
|
||||
<el-form :model="flowDlg" label-width="90px" style="margin-top:12px">
|
||||
<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-input-number v-model="flowDlg.stationNo" :min="1" :max="12" style="width:100%" />
|
||||
<div style="color:#909399;font-size:12px;line-height:1.4">该流程承载的工序编号(1~12)</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="绑定工位" required>
|
||||
<el-select v-model="flowDlg.stations" multiple collapse-tags placeholder="可多选工位:一个工位同一时刻只能绑一个启用流程,一个流程可绑多个工位" 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>
|
||||
<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')"
|
||||
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>
|
||||
type="primary" @click="saveFlow">保存</el-button>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<!-- ============ 工艺流程 新增/编辑 ============ -->
|
||||
<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-input-number v-model="flowDlg.stationNo" :min="1" :max="12" style="width:100%" />
|
||||
<div style="color:#909399;font-size:12px;line-height:1.4">该流程承载的工序编号(1~12)</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="绑定工位" required>
|
||||
<el-select v-model="flowDlg.stations" multiple collapse-tags placeholder="可多选工位:一个工位同一时刻只能绑一个启用流程,一个流程可绑多个工位" 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>
|
||||
|
||||
<!-- ============ 考核标准编辑(每步骤独立弹窗,单独保存) ============ -->
|
||||
<!-- ============ 考核标准编辑(单层弹窗,从配置 tab 打开,不再嵌套在弹窗内) ============ -->
|
||||
<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="为「步骤」设置合格判定标准:报工时按此标准自动判定合格/不合格;未设标准则该步骤仅记录、不判定。" />
|
||||
@@ -223,7 +232,7 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
<PageHelp :data="helpProcessFlow" />
|
||||
<PageHelp :data="helpProcessFlow" page="ProcessFlow" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -233,6 +242,7 @@ import { helpProcessFlow } from '../help'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import request from '../utils/request'
|
||||
|
||||
const activeTab = ref('list')
|
||||
const flows = ref([])
|
||||
const query = reactive({ stationNo: undefined })
|
||||
const pdfInput = ref(null)
|
||||
@@ -306,8 +316,8 @@ async function onPdfChange(e) {
|
||||
}
|
||||
|
||||
const flowDlg = reactive({
|
||||
show: false, id: 0, name: '', stationNo: 1, stations: [], pdfFile: '', status: 'ACTIVE',
|
||||
remark: '', uploading: false, steps: []
|
||||
id: 0, name: '', stationNo: 1, stations: [], pdfFile: '', status: 'ACTIVE',
|
||||
remark: '', uploading: false, steps: [blankStep(1)]
|
||||
})
|
||||
|
||||
function openFlow(row) {
|
||||
@@ -334,7 +344,7 @@ function openFlow(row) {
|
||||
})
|
||||
}
|
||||
if (!flowDlg.steps.length) flowDlg.steps.push(blankStep(1))
|
||||
flowDlg.show = true
|
||||
activeTab.value = 'config'
|
||||
}
|
||||
|
||||
function addStep() {
|
||||
@@ -346,7 +356,7 @@ function removeStep(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'))
|
||||
@@ -397,7 +407,7 @@ async function saveFlow() {
|
||||
}
|
||||
await request.post('/process-flows', body)
|
||||
ElMessage.success('保存成功')
|
||||
flowDlg.show = false
|
||||
activeTab.value = 'list'
|
||||
loadFlows()
|
||||
}
|
||||
|
||||
@@ -418,7 +428,8 @@ async function toggleStatus(row) {
|
||||
} else {
|
||||
await ElMessageBox.confirm(
|
||||
`确认启用「${row.name}」?启用后其绑定工位(${(row.stations || []).map(s => '工位' + s).join('、') || '-'})将恢复使用该流程。`,
|
||||
'提示', { type: 'warning' })
|
||||
'提示', { type: 'warning' }
|
||||
)
|
||||
}
|
||||
await request.post('/process-flows/status', { id: row.id, status: toStatus })
|
||||
ElMessage.success(`已${action}`)
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
<PageHelp :data="helpProductType" />
|
||||
<PageHelp :data="helpProductType" page="ProductType" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
<el-card shadow="never">
|
||||
<el-table :data="roles" border size="small" v-loading="loading" empty-text="暂无角色">
|
||||
<el-table-column prop="id" label="ID" width="64" align="center" />
|
||||
<el-table-column prop="code" label="编码" min-width="120" />
|
||||
<el-table-column label="名称" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.code === 'SUPER_ADMIN' ? 'danger' : 'primary'" size="small">{{ row.name }}</el-tag>
|
||||
@@ -33,6 +34,9 @@
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" width="160" align="center">
|
||||
<template #default="{ row }">{{ fmtTime(row.createdAt) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="can('sys.role:edit')" link type="primary" size="small"
|
||||
@@ -44,7 +48,7 @@
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="dialog.show" :title="dialog.editingId ? '编辑角色权限' : '新增角色'" width="600px"
|
||||
<el-dialog v-model="dialog.show" :title="dialog.editingId ? '编辑角色权限' : '新增角色'" width="560px"
|
||||
append-to-body destroy-on-close>
|
||||
<el-form label-width="90px">
|
||||
<el-form-item label="角色名称" required>
|
||||
@@ -81,6 +85,7 @@ defineOptions({ name: 'RoleManage' })
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import request from '../utils/request'
|
||||
import { fmtTime } from '../utils/format'
|
||||
import { can } from '../utils/perm'
|
||||
import PageHelp from '../components/PageHelp.vue'
|
||||
import { helpRole } from '../help'
|
||||
@@ -96,7 +101,9 @@ const codeName = computed(() => {
|
||||
return m
|
||||
})
|
||||
|
||||
// MES 无 parentCode:按钮码形如 菜单码:动作,父菜单 = 冒号前段(菜单码不含冒号)
|
||||
// 权限树:一级=菜单,二级=该菜单下的按钮。
|
||||
// 父子关系以 permission.parentCode 为准(每个按钮显式声明所属菜单,不用前缀通配推断);
|
||||
// 未声明 parentCode 的按钮归入「未归类」分组,便于发现漏配。
|
||||
const permTree = computed(() => {
|
||||
const menus = perms.value.filter((p) => p.type === 'MENU')
|
||||
const buttons = perms.value.filter((p) => p.type === 'BUTTON')
|
||||
@@ -104,25 +111,31 @@ const permTree = computed(() => {
|
||||
label: m.name,
|
||||
code: m.code,
|
||||
children: buttons
|
||||
.filter((b) => b.code.startsWith(m.code + ':'))
|
||||
.filter((b) => b.parentCode === m.code)
|
||||
.map((b) => ({ label: b.name, code: b.code }))
|
||||
}))
|
||||
const stray = buttons.filter((b) => !menus.some((m) => b.code.startsWith(m.code + ':')))
|
||||
const stray = buttons.filter((b) => !menus.some((m) => m.code === b.parentCode))
|
||||
if (stray.length) {
|
||||
nodes.push({ label: '通用', global: true, children: stray.map((b) => ({ label: b.name, code: b.code })) })
|
||||
nodes.push({ label: '未归类', global: true, children: stray.map((b) => ({ label: b.name, code: b.code })) })
|
||||
}
|
||||
return nodes
|
||||
})
|
||||
|
||||
function permCount(row) {
|
||||
const codes = row.permissionCodes || []
|
||||
if (row.code === 'SUPER_ADMIN') return codes
|
||||
if (codes.includes('*')) return '全部'
|
||||
return codes.filter((c) => c !== '*').length
|
||||
}
|
||||
// 权限明细统一显示中文名(与 WMS 一致);最多显示 8 项,完整内容在 tooltip
|
||||
function permSummary(row) {
|
||||
const codes = row.permissionCodes || []
|
||||
if (codes.includes('*')) return '全部权限'
|
||||
return codes.map((c) => codeName.value[c] || c).join('、') || '-'
|
||||
const codes = (row.permissionCodes || []).filter((c) => c !== '*')
|
||||
if ((row.permissionCodes || []).includes('*')) return '全部权限'
|
||||
return summaryOf(codes)
|
||||
}
|
||||
function summaryOf(codes) {
|
||||
if (!codes.length) return '-'
|
||||
const names = codes.map((c) => codeName.value[c] || c)
|
||||
return names.length > 8 ? `${names.slice(0, 8).join('、')} 等 ${names.length} 项` : names.join('、')
|
||||
}
|
||||
|
||||
async function loadRoles() {
|
||||
|
||||
@@ -44,7 +44,46 @@
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
<el-alert v-else type="info" :closable="false" title="请选择工位、输入SN后点【载入工序步骤】;若提示未配置工序步骤,请到「工艺流程」为该工位启用流程并维护工序步骤" />
|
||||
|
||||
<!-- 装机绑定区:该工件在本工序应装什么料(BOM 装配工序配置)→ 扫码/手录绑定,齐套后才允许报工 -->
|
||||
<el-card v-if="bindPanel" shadow="never" style="margin-bottom:12px;border-color:var(--el-color-primary)">
|
||||
<div slot="header" style="display:flex;justify-content:space-between;align-items:center">
|
||||
<b>工序 {{ form.stationNo }} 装机绑定(装配物料)</b>
|
||||
<el-alert v-if="bindPanel.enabled && !bindPanel.complete" type="warning" :closable="false" show-icon style="flex:1;margin-left:16px"
|
||||
:title="'尚未绑齐:' + bindPanel.missing" />
|
||||
<el-alert v-else-if="bindPanel.enabled && bindPanel.complete" type="success" :closable="false" show-icon style="flex:1;margin-left:16px"
|
||||
title="装配物料已绑齐,可提交报工" />
|
||||
<el-alert v-else type="info" :closable="false" style="flex:1;margin-left:16px"
|
||||
title="该产品未在本工序配置需绑定物料(BOM「装配工序」为空),不校验" />
|
||||
</div>
|
||||
<el-table v-if="bindPanel.required && bindPanel.required.length" :data="bindPanel.required" border size="small">
|
||||
<el-table-column prop="materialCode" label="物料编码" min-width="110" />
|
||||
<el-table-column prop="materialName" label="物料名称" min-width="120" />
|
||||
<el-table-column prop="manageMode" label="管理方式" width="120">
|
||||
<template #default="{row}">{{ row.manageMode === '2' ? '精密件(SN)' : '结构件(批次)' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="已绑/需绑" width="100" align="center">
|
||||
<template #default="{row}">
|
||||
<b :style="{color: row.complete ? 'var(--el-color-success)' : 'var(--el-color-danger)'}">{{ row.boundCount }}/{{ row.need }}</b>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="已绑定值" min-width="170">
|
||||
<template #default="{row}">
|
||||
<el-tag v-for="b in row.bound" :key="b" size="small" style="margin-right:4px">{{ b }}</el-tag>
|
||||
<span v-if="!row.bound.length" style="color:#909399">未绑定</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="绑定操作" width="230">
|
||||
<template #default="{row}">
|
||||
<div style="display:flex;gap:4px">
|
||||
<el-input v-model="row._input" :placeholder="row.manageMode === '2' ? '扫码SN' : '扫码/输入批次号'" size="small" @keyup.enter="bindOne(row)" />
|
||||
<el-button size="small" type="primary" :loading="binding" @click="bindOne(row)">绑定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
<el-alert v-if="!steps.length && !bindPanel" type="info" :closable="false" title="请选择工位、输入SN后点【载入工序步骤】;若提示未配置工序步骤,请到「工艺流程」为该工位启用流程并维护工序步骤" />
|
||||
|
||||
<el-divider content-position="left">报工记录</el-divider>
|
||||
<el-form inline>
|
||||
@@ -62,7 +101,7 @@
|
||||
<el-table-column prop="time" label="时间" min-width="170" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
<PageHelp :data="helpScan" />
|
||||
<PageHelp :data="helpScan" page="Scan" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -77,6 +116,8 @@ const form = reactive({ stationNo: 1, sn: '' })
|
||||
const steps = ref([])
|
||||
const flowName = ref('')
|
||||
const saving = ref(false)
|
||||
const bindPanel = ref(null)
|
||||
const binding = ref(false)
|
||||
const query = reactive({ sn: '', orderNo: '' })
|
||||
const list = ref([])
|
||||
|
||||
@@ -95,6 +136,7 @@ function critText(c) {
|
||||
// 从后端加载该工位启用工艺流程的工序步骤(value 挂在 row 上,提交时收集)
|
||||
async function loadSteps() {
|
||||
if (!form.sn) return ElMessage.warning('请先输入工件SN')
|
||||
bindPanel.value = null
|
||||
try {
|
||||
const data = await request.get('/process-steps', { params: { stationNo: form.stationNo } }) || []
|
||||
steps.value = data.map(s => ({ ...s, value: '', text: '' }))
|
||||
@@ -102,12 +144,50 @@ async function loadSteps() {
|
||||
if (!steps.value.length) {
|
||||
ElMessage.warning(`工位${form.stationNo} 未配置启用的工艺流程,请到「工艺流程」维护`)
|
||||
}
|
||||
loadBindPanel()
|
||||
} catch { steps.value = [] }
|
||||
}
|
||||
|
||||
// 拉取本工件本工序的装配物料绑定面板(应绑清单 + 已绑情况)
|
||||
async function loadBindPanel() {
|
||||
if (!form.sn) return
|
||||
try {
|
||||
const p = await request.get('/bind-panel', { params: { sn: form.sn, processCode: form.stationNo } }) || null
|
||||
if (p?.required) {
|
||||
p.required.forEach((r) => { r._input = '' })
|
||||
}
|
||||
bindPanel.value = p
|
||||
} catch {
|
||||
bindPanel.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// 扫/录单个物料 → 立即绑定落库并刷新齐套状态(防错即时反馈)
|
||||
async function bindOne(row) {
|
||||
const v = (row._input || '').trim()
|
||||
if (!v) return ElMessage.warning('请先输入绑定值(精密件扫码SN / 结构件批次号)')
|
||||
binding.value = true
|
||||
try {
|
||||
await request.post('/binds', {
|
||||
sn: form.sn,
|
||||
orderNo: '',
|
||||
processCode: form.stationNo,
|
||||
stationNo: form.stationNo,
|
||||
items: [{ materialCode: row.materialCode, bindValue: v }]
|
||||
})
|
||||
ElMessage.success('绑定成功')
|
||||
loadBindPanel()
|
||||
} catch { /* 错误已在拦截器提示 */ }
|
||||
binding.value = false
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (!form.sn) return ElMessage.warning('请输入SN')
|
||||
if (!steps.value.length) return ElMessage.warning('该工位未配置工序步骤,无法报工')
|
||||
// 报工前强校验:本工序装配物料需绑齐(后端 validateStationBinds)
|
||||
if (bindPanel.value?.enabled && !bindPanel.value?.complete) {
|
||||
return ElMessage.warning('装配物料未绑齐,请先完成装机绑定:' + (bindPanel.value.missing || ''))
|
||||
}
|
||||
const stepReq = steps.value
|
||||
.filter(s => s.collectType !== 'NONE' && (s.text !== '' || s.value !== ''))
|
||||
.map(s => ({
|
||||
@@ -125,6 +205,7 @@ async function submit() {
|
||||
})
|
||||
ElMessage.success('报工成功')
|
||||
steps.value = []
|
||||
bindPanel.value = null
|
||||
Object.assign(form, { sn: '' })
|
||||
load()
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
<PageHelp :data="helpStation" />
|
||||
<PageHelp :data="helpStation" page="Station" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<el-table-column prop="time" label="时间" min-width="170" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
<PageHelp :data="helpTorque" />
|
||||
<PageHelp :data="helpTorque" page="Torque" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
@@ -57,10 +57,28 @@
|
||||
<p>批次号:{{ (data.associationTrace?.batchItems||[]).join(', ') || '无' }}</p>
|
||||
<p>精密件SN:{{ (data.associationTrace?.serialItems||[]).join(', ') || '无' }}</p>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="装机绑定">
|
||||
<el-alert :type="data.bindTrace?.complete ? 'success' : 'warning'" :closable="false" style="margin-bottom:10px"
|
||||
:title="data.bindTrace?.complete ? '装配物料已全部绑齐 ✓' : ('装配物料未绑齐:' + (data.bindTrace?.missing || ''))" show-icon />
|
||||
<el-table :data="data.bindTrace?.binds||[]" border size="small">
|
||||
<el-table-column prop="processCode" label="工序" width="70" />
|
||||
<el-table-column prop="materialCode" label="物料编码" min-width="110" />
|
||||
<el-table-column prop="materialName" label="物料名称" min-width="110" />
|
||||
<el-table-column prop="spec" label="规格" min-width="90" />
|
||||
<el-table-column prop="bindType" label="类型" width="70">
|
||||
<template #default="{row}">{{ row.bindType === 'SN' ? '精密件SN' : '批次' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="bindValue" label="绑定值(SN/批次)" min-width="140" />
|
||||
<el-table-column prop="operator" label="绑定人" width="90" />
|
||||
<el-table-column prop="createdAt" label="绑定时间" min-width="165">
|
||||
<template #default="{row}">{{ fmt(row.createdAt) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
</el-card>
|
||||
<PageHelp :data="helpTrace" />
|
||||
<PageHelp :data="helpTrace" page="Trace" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
<PageHelp :data="helpWorkOrder" />
|
||||
<PageHelp :data="helpWorkOrder" page="WorkOrder" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// 公共格式化工具(与 WMS utils/format.js 同标准)
|
||||
export function fmtTime(v) {
|
||||
if (!v) return '-'
|
||||
const d = new Date(typeof v === 'number' ? v * 1000 : v)
|
||||
if (Number.isNaN(d.getTime())) return String(v)
|
||||
const p = (n) => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`
|
||||
}
|
||||
|
||||
export function fmtNum(v) {
|
||||
const n = Number(v)
|
||||
return Number.isNaN(n) ? (v ?? '-') : n.toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
export function fmtDateOnly(v) {
|
||||
if (!v) return '-'
|
||||
const s = String(v)
|
||||
if (/^\d+$/.test(s)) return fmtTime(Number(s)).slice(0, 10)
|
||||
return s.slice(0, 10)
|
||||
}
|
||||
Reference in New Issue
Block a user