feat(mes): 添加报工人和审核人字段,优化审批逻辑

pull/871/MERGE
YunaiV 2026-03-19 08:40:56 +08:00
parent 6553ab1875
commit 424f3d04c1
5 changed files with 198 additions and 322 deletions

View File

@ -24,7 +24,7 @@ export interface ProFeedbackVO {
itemId: number // 产品物料编号
itemCode: string // 物料编码
itemName: string // 物料名称
itemSpec: string // 规格型号
itemSpecification: string // 规格型号
unitMeasureId: number // 单位编号
unitMeasureName: string // 单位名称
expireDate: Date // 过期日期

View File

@ -15,7 +15,7 @@ export interface ProRouteProcessVO {
waitTime?: number // 等待时间(分钟)
colorCode?: string // 甘特图显示颜色
keyFlag?: number // 是否关键工序
checkFlag?: number // 是否质检工序
checkFlag?: boolean // 是否质检工序
remark?: string // 备注
createTime?: Date // 创建时间
}
@ -37,6 +37,14 @@ export const ProRouteProcessApi = {
return await request.get({ url: `/mes/pro/route-process/get?id=` + id })
},
// 按工艺路线+工序精确查询工序配置
getRouteProcessByRouteAndProcess: async (routeId: number, processId: number) => {
return await request.get({
url: `/mes/pro/route-process/get-by-route-and-process`,
params: { routeId, processId }
})
},
// 新增工艺路线工序
createRouteProcess: async (data: ProRouteProcessVO) => {
return await request.post({ url: `/mes/pro/route-process/create`, data })

View File

@ -1,197 +0,0 @@
<!-- MES 物料产品 弹窗选择器 -->
<template>
<Dialog title="物料产品选择" v-model="dialogVisible" width="80%">
<el-row :gutter="20">
<!-- 左侧分类树 -->
<el-col :span="5">
<el-input
v-model="filterText"
placeholder="搜索分类"
clearable
class="mb-12px"
:prefix-icon="iconSearch"
/>
<el-tree
ref="treeRef"
:data="itemTypeTree"
:props="treeProps"
:expand-on-click-node="false"
:filter-node-method="filterNode"
default-expand-all
highlight-current
@node-click="handleNodeClick"
/>
</el-col>
<!-- 右侧物料表格 -->
<el-col :span="19">
<!-- 搜索表单 -->
<el-form :inline="true" :model="queryParams" class="mb-10px" label-width="80px">
<el-form-item label="物料编码">
<el-input
v-model="queryParams.code"
placeholder="请输入物料编码"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="物料名称">
<el-input
v-model="queryParams.name"
placeholder="请输入物料名称"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">
<Icon icon="ep:search" class="mr-5px" /> 搜索
</el-button>
<el-button @click="resetQuery">
<Icon icon="ep:refresh" class="mr-5px" /> 重置
</el-button>
</el-form-item>
</el-form>
<!-- 数据表格 -->
<el-table
v-loading="loading"
:data="list"
:stripe="true"
:show-overflow-tooltip="true"
border
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="50" align="center" />
<el-table-column label="物料编码" align="center" prop="code" width="120" />
<el-table-column label="物料名称" align="center" prop="name" min-width="120" />
<el-table-column label="规格型号" align="center" prop="specification" />
<el-table-column label="单位" align="center" prop="unitMeasureName" width="80" />
<el-table-column label="物料/产品" align="center" prop="itemOrProduct" width="100">
<template #default="scope">
{{ getItemOrProductLabel(scope.row.itemOrProduct) }}
</template>
</el-table-column>
<el-table-column label="所属分类" align="center" prop="itemTypeName" width="120" />
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</el-col>
</el-row>
<template #footer>
<el-button type="primary" @click="confirmSelect"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { MdItemApi, MdItemVO } from '@/api/mes/md/item'
import { MdItemTypeApi, MdItemTypeVO } from '@/api/mes/md/item/type'
import { handleTree } from '@/utils/tree'
import { getItemOrProductLabel } from '@/views/mes/utils/constants'
import { Search as iconSearch } from '@element-plus/icons-vue'
defineOptions({ name: 'ItemProductSelect' })
const message = useMessage() //
const emit = defineEmits<{
selected: [rows: MdItemVO[]] //
}>()
const dialogVisible = ref(false) //
const loading = ref(false) //
const list = ref<MdItemVO[]>([]) //
const total = ref(0) //
const selectedRows = ref<MdItemVO[]>([]) //
// ==================== ====================
const treeRef = ref() // Ref
const filterText = ref('') //
const itemTypeTree = ref<MdItemTypeVO[]>([]) //
const treeProps = { children: 'children', label: 'name' } //
/** 过滤树节点 */
const filterNode = (value: string, data: MdItemTypeVO) => {
if (!value) return true
return data.name?.includes(value)
}
/** 监听筛选文本变化 */
watch(filterText, (val) => {
treeRef.value?.filter(val)
})
/** 点击树节点 */
const handleNodeClick = (data: MdItemTypeVO) => {
queryParams.itemTypeId = data.id
handleQuery()
}
// ==================== ====================
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
code: undefined as string | undefined,
name: undefined as string | undefined,
itemTypeId: undefined as number | undefined
})
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await MdItemApi.getItemPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置 */
const resetQuery = () => {
queryParams.code = undefined
queryParams.name = undefined
queryParams.itemTypeId = undefined
handleQuery()
}
/** 选中变化 */
const handleSelectionChange = (rows: MdItemVO[]) => {
selectedRows.value = rows
}
/** 确认选择 */
const confirmSelect = () => {
if (selectedRows.value.length === 0) {
message.warning('请至少选择一条数据')
return
}
emit('selected', selectedRows.value)
dialogVisible.value = false
}
// ==================== ====================
/** 打开弹窗 */
const open = async () => {
dialogVisible.value = true
selectedRows.value = []
//
const typeList = await MdItemTypeApi.getItemTypeSimpleList()
itemTypeTree.value = handleTree(typeList)
//
await getList()
}
defineExpose({ open }) // open
</script>

View File

@ -66,17 +66,48 @@
</el-form-item>
</el-col>
</el-row>
<!-- 工序 -->
<el-row :gutter="20">
<!-- 数量区域 -->
<el-divider content-position="left">报工数量</el-divider>
<!-- 非质检工序报工数量(只读自动计算) + 合格品 + 不良品 -->
<el-row :gutter="20" v-if="!checkFlag">
<el-col :span="8">
<el-form-item label="工序" prop="processId">
<el-input v-model="processDisplay" disabled placeholder="由任务自动带入" />
<el-form-item label="报工数量" prop="feedbackQuantity">
<el-input-number
v-model="formData.feedbackQuantity"
:min="0"
:precision="2"
disabled
class="!w-1/1"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="合格品数量" prop="qualifiedQuantity">
<el-input-number
v-model="formData.qualifiedQuantity"
:min="0"
:precision="2"
:disabled="isDetail"
class="!w-1/1"
@change="handleQuantityChanged"
/>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="不良品数量" prop="unqualifiedQuantity">
<el-input-number
v-model="formData.unqualifiedQuantity"
:min="0"
:precision="2"
:disabled="isDetail"
class="!w-1/1"
@change="handleQuantityChanged"
/>
</el-form-item>
</el-col>
</el-row>
<!-- 数量区域 -->
<el-divider content-position="left">报工数量</el-divider>
<el-row :gutter="20">
<!-- 质检工序只填报工数量 -->
<el-row :gutter="20" v-else>
<el-col :span="8">
<el-form-item label="报工数量" prop="feedbackQuantity">
<el-input-number
@ -88,46 +119,9 @@
/>
</el-form-item>
</el-col>
<!-- TODO @芋艿在评审下 -->
<el-col :span="8" v-if="!checkFlag">
<el-form-item label="合格品数量" prop="qualifiedQuantity">
<el-input-number
v-model="formData.qualifiedQuantity"
:min="0"
:precision="2"
:disabled="isDetail"
class="!w-1/1"
/>
</el-form-item>
</el-col>
<!-- TODO @芋艿在评审下 -->
<el-col :span="8" v-if="!checkFlag">
<el-form-item label="不良品数量" prop="unqualifiedQuantity">
<el-input-number
v-model="formData.unqualifiedQuantity"
:min="0"
:precision="2"
:disabled="isDetail"
class="!w-1/1"
/>
</el-form-item>
</el-col>
<!-- TODO @芋艿在评审下 -->
<el-col :span="8" v-if="checkFlag">
<el-form-item label="待检测数量">
<el-input-number
v-model="formData.uncheckQuantity"
:min="0"
:precision="2"
:disabled="isDetail"
class="!w-1/1"
/>
</el-form-item>
</el-col>
</el-row>
<!-- 废品分类不良品>0 时展开 -->
<!-- TODO @芋艿在评审下 -->
<el-row :gutter="20" v-if="formData.unqualifiedQuantity > 0">
<!-- 废品分类非质检工序 不良品>0 时展开 -->
<el-row :gutter="20" v-if="!checkFlag && formData.unqualifiedQuantity > 0">
<el-col :span="8">
<el-form-item label="工废数量">
<el-input-number
@ -136,6 +130,7 @@
:precision="2"
:disabled="isDetail"
class="!w-1/1"
@change="handleScrapChanged"
/>
</el-form-item>
</el-col>
@ -147,6 +142,7 @@
:precision="2"
:disabled="isDetail"
class="!w-1/1"
@change="handleScrapChanged"
/>
</el-form-item>
</el-col>
@ -158,6 +154,7 @@
:precision="2"
:disabled="isDetail"
class="!w-1/1"
@change="handleScrapChanged"
/>
</el-form-item>
</el-col>
@ -178,7 +175,7 @@
<el-date-picker
v-model="formData.feedbackTime"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
value-format="x"
placeholder="请选择报工时间"
:disabled="isDetail"
class="!w-1/1"
@ -218,22 +215,32 @@
</template>
<script setup lang="ts">
import { getIntDictOptions, getStrDictOptions, DICT_TYPE } from '@/utils/dict'
import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
import { ProFeedbackApi, ProFeedbackVO } from '@/api/mes/pro/feedback'
import { ProRouteProcessApi } from '@/api/mes/pro/route/process'
import ProWorkOrderSelect from '@/views/mes/pro/workorder/components/ProWorkOrderSelect.vue'
import ProTaskSelect from '@/views/mes/pro/task/components/ProTaskSelect.vue'
import MdWorkstationSelect from '@/views/mes/md/workstation/components/MdWorkstationSelect.vue'
import UserSelect from '@/views/system/user/components/UserSelect.vue'
import { useUserStore } from '@/store/modules/user'
defineOptions({ name: 'FeedbackForm' })
const { t } = useI18n()
const message = useMessage()
const { t } = useI18n() //
const message = useMessage() //
const dialogVisible = ref(false)
const dialogTitle = ref('')
const formLoading = ref(false)
const formType = ref('') // 'create' | 'update' | 'detail'
const dialogVisible = ref(false) //
const formLoading = ref(false) //
const formType = ref('') // create - update - detail -
const dialogTitle = computed(() => {
if (formType.value === 'detail') {
return '查看生产报工记录'
}
if (formType.value === 'create') {
return '添加生产报工记录'
}
return '修改生产报工记录'
})
const formData = ref<Record<string, any>>({
id: undefined,
code: undefined,
@ -243,6 +250,7 @@ const formData = ref<Record<string, any>>({
processId: undefined,
workOrderId: undefined,
taskId: undefined,
itemId: undefined,
expireDate: undefined,
feedbackQuantity: 0,
qualifiedQuantity: 0,
@ -261,46 +269,72 @@ const formRules = reactive({
workOrderId: [{ required: true, message: '生产工单不能为空', trigger: 'change' }],
taskId: [{ required: true, message: '生产任务不能为空', trigger: 'change' }],
workstationId: [{ required: true, message: '工作站不能为空', trigger: 'change' }],
feedbackQuantity: [{ required: true, message: '报工数量不能为空', trigger: 'blur' }]
feedbackQuantity: [{ required: true, message: '报工数量不能为空', trigger: 'blur' }],
feedbackUserId: [{ required: true, message: '报工人不能为空', trigger: 'change' }],
feedbackTime: [{ required: true, message: '报工时间不能为空', trigger: 'change' }],
approveUserId: [{ required: true, message: '审核人不能为空', trigger: 'change' }]
})
const formRef = ref()
/** 是否为详情模式 */
const isDetail = computed(() => formType.value === 'detail')
/** 是否需要检验checkFlag */
const checkFlag = ref(false)
/** 工序显示(只读) */
const processDisplay = ref('')
const formRef = ref() // Ref
const isDetail = computed(() => formType.value === 'detail') //
const checkFlag = ref(true) // true
// ==================== ====================
/** 加载工序的 checkFlag */
const loadCheckFlag = async (routeId?: number, processId?: number) => {
if (!routeId || !processId) {
checkFlag.value = true
return
}
try {
const routeProcess = await ProRouteProcessApi.getRouteProcessByRouteAndProcess(
routeId,
processId
)
checkFlag.value = routeProcess?.checkFlag ?? false
} catch {
checkFlag.value = true
}
}
/** 合格品/不良品变更:自动计算报工数量 = 合格 + 不良 */
const handleQuantityChanged = () => {
formData.value.feedbackQuantity =
(formData.value.qualifiedQuantity || 0) + (formData.value.unqualifiedQuantity || 0)
}
/** 废品明细变更:自动计算不良品数量 = 工废 + 料废 + 其他 */
const handleScrapChanged = () => {
formData.value.unqualifiedQuantity =
(formData.value.laborScrapQuantity || 0) +
(formData.value.materialScrapQuantity || 0) +
(formData.value.otherScrapQuantity || 0)
handleQuantityChanged()
}
/** 工单变更:清空任务相关字段 */
const handleWorkOrderChange = () => {
formData.value.taskId = undefined
formData.value.routeId = undefined
formData.value.processId = undefined
formData.value.workstationId = undefined
processDisplay.value = ''
checkFlag.value = false
formData.value.itemId = undefined
checkFlag.value = true
}
/** 任务变更:自动填充关联字段 */
const handleTaskChange = (task: any) => {
const handleTaskChange = async (task: any) => {
if (!task) {
return
}
formData.value.routeId = task.routeId
formData.value.processId = task.processId
formData.value.workstationId = task.workstationId
processDisplay.value = task.processCode ? task.processCode + ' - ' + task.processName : ''
// TODO @ checkFlag routeProcess
formData.value.itemId = task.itemId
await loadCheckFlag(task.routeId, task.processId)
}
// ==================== ====================
/** 生成报工单编号(前端生成) */
/** 生成报工单编号 */
// TODO @
const generateCode = () => {
const now = new Date()
@ -323,33 +357,26 @@ const generateCode = () => {
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
// TODO @AI computed
dialogTitle.value =
type === 'detail'
? '查看生产报工记录'
: type === 'create'
? '添加生产报工记录'
: '修改生产报工记录'
formType.value = type
resetForm()
// /
if (id) {
formLoading.value = true
try {
const data = await ProFeedbackApi.getFeedback(id)
formData.value = data as any
//
processDisplay.value = data.processCode ? data.processCode + ' - ' + data.processName : ''
checkFlag.value = (data as any).checkFlag || false
await loadCheckFlag(data.routeId, data.processId)
} finally {
formLoading.value = false
}
} else {
//
// +
formData.value.code = generateCode()
formData.value.feedbackUserId = useUserStore().getUser.id
}
}
defineExpose({ open })
defineExpose({ open }) // open
/** 提交表单 */
const emit = defineEmits(['success'])
@ -358,6 +385,21 @@ const submitForm = async () => {
formLoading.value = true
try {
const data = formData.value as unknown as ProFeedbackVO
// checkFlag
if (checkFlag.value) {
// //
data.uncheckQuantity = data.feedbackQuantity
data.qualifiedQuantity = 0
data.unqualifiedQuantity = 0
data.laborScrapQuantity = 0
data.materialScrapQuantity = 0
data.otherScrapQuantity = 0
} else {
// = +
data.feedbackQuantity = (data.qualifiedQuantity || 0) + (data.unqualifiedQuantity || 0)
data.uncheckQuantity = 0
}
//
if (formType.value === 'create') {
await ProFeedbackApi.createFeedback(data)
message.success(t('common.createSuccess'))
@ -366,6 +408,7 @@ const submitForm = async () => {
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
//
emit('success')
} finally {
formLoading.value = false
@ -383,6 +426,7 @@ const resetForm = () => {
processId: undefined,
workOrderId: undefined,
taskId: undefined,
itemId: undefined,
expireDate: undefined,
feedbackQuantity: 0,
qualifiedQuantity: 0,
@ -396,8 +440,7 @@ const resetForm = () => {
approveUserId: undefined,
remark: undefined
}
processDisplay.value = ''
checkFlag.value = false
checkFlag.value = true
formRef.value?.resetFields()
}
</script>

View File

@ -34,13 +34,21 @@
</el-select>
</el-form-item>
<el-form-item label="生产工单" prop="workOrderId">
<ProWorkOrderSelect v-model="queryParams.workOrderId" placeholder="请选择工单" class="!w-240px" />
<ProWorkOrderSelect
v-model="queryParams.workOrderId"
placeholder="请选择工单"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="产品物料" prop="itemId">
<MdItemSelect v-model="queryParams.itemId" placeholder="请选择产品物料" class="!w-240px" />
</el-form-item>
<el-form-item label="报工人" prop="feedbackUserId">
<UserSelect v-model="queryParams.feedbackUserId" placeholder="请选择报工人" class="!w-240px" />
<UserSelect
v-model="queryParams.feedbackUserId"
placeholder="请选择报工人"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="记录人" prop="creator">
<UserSelect v-model="queryParams.creator" placeholder="请选择记录人" class="!w-240px" />
@ -99,8 +107,14 @@
:show-overflow-tooltip="true"
row-key="id"
>
<!-- TODO @AI这里点击后跳转详情然后去掉下面的详情按钮 -->
<el-table-column label="报工单号" align="center" prop="code" width="160" />
<!-- DONE @AI这里点击后跳转详情然后去掉下面的详情按钮 -->
<el-table-column label="报工单号" align="center" prop="code" width="160">
<template #default="scope">
<el-button link type="primary" @click="openForm('detail', scope.row.id)">{{
scope.row.code
}}</el-button>
</template>
</el-table-column>
<el-table-column label="报工类型" align="center" prop="type" width="100">
<template #default="scope">
<dict-tag :type="DICT_TYPE.MES_PRO_FEEDBACK_TYPE" :value="scope.row.type" />
@ -111,7 +125,7 @@
<el-table-column label="生产工单编码" align="center" prop="workOrderCode" width="160" />
<el-table-column label="产品物料编码" align="center" prop="itemCode" width="120" />
<el-table-column label="产品物料名称" align="center" prop="itemName" width="120" />
<el-table-column label="规格型号" align="center" prop="itemSpec" width="120" />
<el-table-column label="规格型号" align="center" prop="itemSpecification" width="120" />
<el-table-column label="单位" align="center" prop="unitMeasureName" width="80" />
<el-table-column label="报工数量" align="center" prop="feedbackQuantity" width="100" />
<el-table-column label="报工人" align="center" prop="feedbackUserNickname" width="100" />
@ -158,33 +172,18 @@
</el-button>
</template>
<!-- 审批中状态驳回执行取消 -->
<!-- TODO @AI审批驳回融合点击后弹出一个界面然后里面在通过不通过 -->
<!-- DONE @AI审批驳回融合点击后弹出一个界面里面通过/不通过 -->
<!-- TODO @AI弹出继续是 Form 组件参考 /Users/yunai/Java/yudao-all-in-one/yudao-ui-admin-vue3/src/views/mes/wm/returnvendor/ReturnVendorForm.vue -->
<template v-if="scope.row.status === MesProFeedbackStatusEnum.APPROVING">
<el-button
link
type="warning"
@click="handleReject(scope.row.id)"
v-hasPermi="['mes:pro-feedback:update']"
>
驳回
</el-button>
<el-button
link
type="success"
@click="handleApprove(scope.row.id)"
type="primary"
@click="openApproveDialog(scope.row.id)"
v-hasPermi="['mes:pro-feedback:approve']"
>
审批
</el-button>
</template>
<!-- 所有状态详情 -->
<el-button
link
type="primary"
@click="openForm('detail', scope.row.id)"
v-hasPermi="['mes:pro-feedback:query']"
>详情</el-button
>
</template>
</el-table-column>
</el-table>
@ -199,6 +198,17 @@
<!-- 表单弹窗添加/修改 -->
<FeedbackForm ref="formRef" @success="getList" />
<!-- 审批弹窗 -->
<Dialog title="审批报工" v-model="approveDialogVisible" width="400px">
<div style="text-align: center; padding: 20px 0">
<p style="margin-bottom: 20px; font-size: 14px">请确认审批结果</p>
<el-button type="success" size="large" @click="handleApprove"></el-button>
<el-button type="danger" size="large" @click="handleReject" style="margin-left: 24px"
>不通过</el-button
>
</div>
</Dialog>
</template>
<script setup lang="ts">
@ -279,24 +289,36 @@ const handleSubmit = async (id: number) => {
} catch {}
}
const handleReject = async (id: number) => {
// ==================== ====================
const approveDialogVisible = ref(false)
const approveTargetId = ref<number>()
const openApproveDialog = (id: number) => {
approveTargetId.value = id
approveDialogVisible.value = true
}
const handleApprove = async () => {
try {
await message.confirm('确认要驳回该报工单吗?')
await ProFeedbackApi.rejectFeedback(id)
message.success('报工单已驳回')
const id = approveTargetId.value!
approveDialogVisible.value = false
const finished = await ProFeedbackApi.approveFeedback(id)
if (finished) {
message.success('报工单已审批完成')
} else {
message.success('报工成功,请等待质量检验完成!')
}
await getList()
} catch {}
}
const handleApprove = async (id: number) => {
const handleReject = async () => {
try {
await message.confirm('确认要审批该报工单吗?')
const status = await ProFeedbackApi.approveFeedback(id)
if (status === MesProFeedbackStatusEnum.UNCHECK) {
message.success('报工成功,请等待质量检验完成!')
} else {
message.success('报工单已审批完成')
}
const id = approveTargetId.value!
approveDialogVisible.value = false
await ProFeedbackApi.rejectFeedback(id)
message.success('报工单已驳回')
await getList()
} catch {}
}