生产订单与报工模块改造汇总 (Git Commit 文档)
feat(mes-pro): 生产订单及报工模块审批流与批次填报改造
1. 生产订单功能增强 (Work Order)
新增字段支持: 生产订单新增 feedbackApprove (是否报工审批)、processDefinitionKey (审批流程定义) 及 feedbackRole (报工角色) 字段。
表单控件完善: 生产订单新增/编辑表单中,添加了对应的流程模型选择下拉框与多选报工角色下拉组件。
任务分配联动: 生产排产报工页面中,新增生产任务选择报工人列表时,现已支持根据生产订单配置的 feedbackRole 去动态拉取并过滤用户;若未配置则降级使用默认角色。
2. 报工接口参数升级 (Feedback Submission)
参数结构调整: 根据后端 /mes/pro/feedback/submit 接口的最新文档,将原先的 ?id=xxx 传参方式改造为 JSON Body 传参 (MesProFeedbackSubmitReqVO),以支持在无需审批场景下直接提交批次号、生产日期等扩展属性。
3. 报工单提交与批次弹框交互优化 (Feedback Forms)
填报流程解耦: 在 FeedbackTab.vue 和 form.vue 中重构了提交与审批逻辑。点击“提交”或“审批通过”时,系统会先请求 getBatchRequirements 检查是否需要填写批次属性。
按需显示批次弹窗: 若需要填报,弹出 BatchInputDialog 收集信息;完成后再将属性与工单 ID 合并提交至后端。
审批跳过机制 (feedbackApprove): 在表单提交阶段,系统会判断所属生产订单的 feedbackApprove 字段。如果启用了报工审批,则在“提交”阶段跳过批次填报,直接提交单据,将填报动作留给审批环节处理。
时序与状态修复: 修复了此前“保存并提交”时会提前弹出成功提示并重置表单,导致批次弹窗内丢失 feedbackId 的问题。现已引入 pendingSubmitMode 与阻塞式提交回调 (handleBatchConfirm),确保所有异步接口和弹窗流转完毕后再统一展示提示并重置状态。
涉及的主要文件:
src/views/mes/pro/workorder/... (生产订单表单)
src/api/mes/pro/feedback/index.ts (API 定义)
src/views/mes/pro/feedback/modules/form.vue (报工台账表单)
src/views/mes/workbench/components/tabs/FeedbackTab.vue (工作台报工页)
src/views/mes/pro/feedback/modules/batch-input-dialog.vue (批次属性录入)
| | |
| | | <excludeFolder url="file://$MODULE_DIR$/.cache" /> |
| | | <excludeFolder url="file://$MODULE_DIR$/dist" /> |
| | | <excludeFolder url="file://$MODULE_DIR$/build" /> |
| | | <excludeFolder url="file://$MODULE_DIR$/.agents" /> |
| | | <excludeFolder url="file://$MODULE_DIR$/.codex" /> |
| | | </content> |
| | | <orderEntry type="inheritedJdk" /> |
| | | <orderEntry type="sourceFolder" forTests="false" /> |
| | |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace MesProFeedbackApi { |
| | | export interface FeedbackSubmitReqVO { |
| | | id: number; |
| | | produceDate?: string; |
| | | expireDate?: string; |
| | | receiptDate?: string; |
| | | vendorId?: number; |
| | | clientId?: number; |
| | | purchaseOrderCode?: string; |
| | | salesOrderCode?: string; |
| | | toolId?: number; |
| | | moldId?: number; |
| | | lotNumber?: string; |
| | | qualityStatus?: number; |
| | | } |
| | | |
| | | /** MES ç产æ¥å·¥ */ |
| | | export interface Feedback { |
| | | id?: number; |
| | |
| | | remark?: string; // 夿³¨ |
| | | creator?: string; // å建人 |
| | | createTime?: number; // å建æ¶é´ |
| | | processInstanceId?: string; // BPM æµç¨å®ä¾ç¼å· |
| | | processDefinitionKey?: string; // BPM æµç¨å®ä¹ Key |
| | | } |
| | | |
| | | /** MES ç产æ¥å·¥å页æ¥è¯¢åæ° */ |
| | |
| | | } |
| | | |
| | | /** æäº¤ç产æ¥å·¥ */ |
| | | export function submitFeedback(id: number) { |
| | | return requestClient.put(`/mes/pro/feedback/submit?id=${id}`); |
| | | export function submitFeedback(data: MesProFeedbackApi.FeedbackSubmitReqVO) { |
| | | return requestClient.put(`/mes/pro/feedback/submit`, data); |
| | | } |
| | | |
| | | /** 驳åç产æ¥å·¥ */ |
| | |
| | | completionProgress?: number; // 宿è¿åº¦ |
| | | remark?: string; // 夿³¨ |
| | | createTime?: number; // å建æ¶é´ |
| | | feedbackApprove?: boolean; // æ¯å¦æ¥å·¥å®¡æ¹ |
| | | processDefinitionKey?: string; // æ¥å·¥å®¡æ¹æµç¨å®ä¹ Key |
| | | feedbackRole?: string; // æ¥å·¥è§è² |
| | | } |
| | | |
| | | /** MES ç产订åå页æ¥è¯¢åæ° */ |
| | |
| | | `/mes/pro/work-order/check-auto-finish/${id}`, |
| | | ); |
| | | } |
| | | |
| | | /** è·å¾ç产订åå®¡æ¹æµç¨å表 */ |
| | | export function getWorkOrderApproveProcessList() { |
| | | return requestClient.get<Array<{ id: string; name: string; key: string }>>( |
| | | '/mes/pro/work-order/approve-process-list', |
| | | ); |
| | | } |
| | |
| | | } |
| | | |
| | | /** éè¿è§è²æ è¯è·åç¨æ·ç²¾ç®ä¿¡æ¯å表 */ |
| | | export function getSimpleUserListByRoleCode(code: string) { |
| | | export function getSimpleUserListByRoleCode(code: Object) { |
| | | return requestClient.get<SystemUserApi.UserSimple[]>('/system/user/list-by-role-code', { |
| | | params: { code }, |
| | | params: code , |
| | | }); |
| | | } |
| | |
| | | }, |
| | | component: () => import('#/views/mes/pd/project/detail/index.vue'), |
| | | }, |
| | | { |
| | | path: 'pro/feedback/detail/:id', |
| | | name: 'MesProFeedbackDetail', |
| | | meta: { |
| | | title: 'ç产æ¥å·¥è¯¦æ
', |
| | | activePath: '/mes/pro/feedback', |
| | | }, |
| | | props: true, |
| | | component: () => import('#/views/mes/pro/feedback/detail/index.vue'), |
| | | }, |
| | | ], |
| | | }, |
| | | ]; |
| | |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { MesProFeedbackApi } from '#/api/mes/pro/feedback'; |
| | | import type { MesProTaskApi } from '#/api/mes/pro/task'; |
| | | import type { DescriptionItemSchema } from '#/components/description'; |
| | | |
| | | import { h, markRaw } from 'vue'; |
| | | |
| | |
| | | import { MdWorkstationSelect } from '#/views/mes/md/workstation/components'; |
| | | import { ProTaskSelect } from '#/views/mes/pro/task/components'; |
| | | import { ProWorkOrderSelect } from '#/views/mes/pro/workorder/components'; |
| | | import { DictTag } from '#/components/dict-tag'; |
| | | |
| | | /** ç产æ¥å·¥è¡¨åç±»å */ |
| | | export type FormType = 'approve' | 'create' | 'detail' | 'submit' | 'update'; |
| | |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** ç产æ¥å·¥è¯¦æ
æè¦ */ |
| | | export function useDetailSchema(): DescriptionItemSchema[] { |
| | | return [ |
| | | { field: 'code', label: 'æ¥è®¢åå·' }, |
| | | { |
| | | field: 'type', |
| | | label: 'æ¥å·¥ç±»å', |
| | | render: (val) => |
| | | h(DictTag, { type: DICT_TYPE.MES_PRO_FEEDBACK_TYPE, value: val }), |
| | | }, |
| | | { field: 'workOrderId', label: 'ç产订å', render: (_val, data) => data.workOrderCode }, |
| | | { field: 'taskId', label: 'ç产任å¡', render: (_val, data) => data.taskCode }, |
| | | { field: 'workstationName', label: 'å·¥ä½ç«' }, |
| | | { field: 'itemCode', label: '产åç¼ç ' }, |
| | | { field: 'itemName', label: '产ååç§°' }, |
| | | { field: 'unitMeasureName', label: 'åä½' }, |
| | | { field: 'itemSpecification', label: 'è§æ ¼' }, |
| | | { field: 'reportableQuantity', label: '坿¥å·¥æ°é' }, |
| | | { field: 'feedbackQuantity', label: 'æ¥å·¥æ°é' }, |
| | | { field: 'qualifiedQuantity', label: 'åæ ¼åæ°é' }, |
| | | { field: 'unqualifiedQuantity', label: 'ä¸è¯åæ°é' }, |
| | | { field: 'laborScrapQuantity', label: 'å·¥åºæ°é' }, |
| | | { field: 'materialScrapQuantity', label: 'æåºæ°é' }, |
| | | { field: 'otherScrapQuantity', label: 'å
¶ä»åºå' }, |
| | | { field: 'feedbackUserNickname', label: 'æ¥å·¥äºº' }, |
| | | { field: 'feedbackTime', label: 'æ¥å·¥æ¶é´' }, |
| | | { field: 'approveUserNickname', label: 'å®¡æ ¸äºº' }, |
| | | { field: 'remark', label: '夿³¨' }, |
| | | ]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { MesProFeedbackApi } from '#/api/mes/pro/feedback'; |
| | | |
| | | import { onMounted, ref } from 'vue'; |
| | | import { useRouter } from 'vue-router'; |
| | | |
| | | import { Page } from '@vben/common-ui'; |
| | | import { useTabs } from '@vben/hooks'; |
| | | |
| | | import { Card, Tabs } from 'ant-design-vue'; |
| | | |
| | | import { TableAction } from '#/adapter/vxe-table'; |
| | | import { getFeedback } from '#/api/mes/pro/feedback'; |
| | | import { useDescription } from '#/components/description'; |
| | | |
| | | import { useDetailSchema } from '../data'; |
| | | import ItemConsumeList from '../modules/item-consume-list.vue'; |
| | | import ProductProduceList from '../modules/product-produce-list.vue'; |
| | | |
| | | const props = defineProps<{ id: string }>(); |
| | | |
| | | const router = useRouter(); |
| | | const tabs = useTabs(); |
| | | |
| | | const loading = ref(false); |
| | | const feedbackId = ref(0); |
| | | const feedback = ref<MesProFeedbackApi.Feedback>({}); |
| | | const activeTab = ref('itemConsume'); |
| | | |
| | | const [Descriptions] = useDescription({ |
| | | bordered: false, |
| | | column: 4, |
| | | schema: useDetailSchema(), |
| | | }); |
| | | |
| | | async function loadFeedbackDetail() { |
| | | loading.value = true; |
| | | try { |
| | | feedback.value = await getFeedback(feedbackId.value); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | function handleBack() { |
| | | tabs.closeCurrentTab(); |
| | | router.push({ path: '/mes/pro/feedback' }); |
| | | } |
| | | |
| | | onMounted(() => { |
| | | feedbackId.value = Number(props.id); |
| | | loadFeedbackDetail(); |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page |
| | | auto-content-height |
| | | :loading="loading" |
| | | :title="feedback.code ? `ç产æ¥å·¥è¯¦æ
- ${feedback.code}` : 'ç产æ¥å·¥è¯¦æ
'" |
| | | > |
| | | <Card class="min-h-[10%]"> |
| | | <Descriptions :data="feedback" /> |
| | | </Card> |
| | | |
| | | </Page> |
| | | </template> |
| | |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { ref } from 'vue'; |
| | | import { useRouter } from 'vue-router'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | |
| | | |
| | | const userStore = useUserStore(); |
| | | const currentUserId = userStore.userInfo?.id; // å½åç»å½ç¨æ· IDï¼ç¨äºå®¡æ¹äººæé夿 |
| | | const router = useRouter(); |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | |
| | | /** 审æ¹ç产æ¥å·¥ */ |
| | | function handleApprove(row: MesProFeedbackApi.Feedback) { |
| | | formModalApi.setData({ formType: 'approve', id: row.id }).open(); |
| | | } |
| | | |
| | | /** æ¥ç审æ¹è¯¦æ
*/ |
| | | function handleViewProcess(row: MesProFeedbackApi.Feedback) { |
| | | if (!row.processInstanceId) { |
| | | message.warning('该æ¥å·¥å°æªæäº¤å®¡æ¹'); |
| | | return; |
| | | } |
| | | router.push({ path: '/bpm/process-instance/detail', query: { id: row.processInstanceId } }); |
| | | } |
| | | |
| | | /** 详æ
ç产æ¥å·¥ */ |
| | |
| | | ifShow: () => row.status === MesProFeedbackStatusEnum.PREPARE, |
| | | onClick: handleSubmit.bind(null, row), |
| | | }, |
| | | // { |
| | | // label: $t('common.approve'), |
| | | // type: 'link', |
| | | // auth: ['mes:pro-feedback:approve'], |
| | | // ifShow: () => |
| | | // row.status === MesProFeedbackStatusEnum.APPROVING && |
| | | // row.approveUserId === currentUserId, |
| | | // onClick: handleApprove.bind(null, row), |
| | | // }, |
| | | { |
| | | label: $t('common.approve'), |
| | | label: 'æ¥ç审æ¹', |
| | | type: 'link', |
| | | auth: ['mes:pro-feedback:approve'], |
| | | ifShow: () => |
| | | row.status === MesProFeedbackStatusEnum.APPROVING && |
| | | row.approveUserId === currentUserId, |
| | | onClick: handleApprove.bind(null, row), |
| | | icon: ACTION_ICON.VIEW, |
| | | ifShow: () => row.status === MesProFeedbackStatusEnum.APPROVING, |
| | | onClick: handleViewProcess.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.delete'), |
| | |
| | | <script lang="ts" setup> |
| | | <script lang="ts" setup> |
| | | import type { FormType } from '../data'; |
| | | |
| | | import type { MesProFeedbackApi } from '#/api/mes/pro/feedback'; |
| | |
| | | updateFeedback, |
| | | } from '#/api/mes/pro/feedback'; |
| | | import { getRouteProcessByRouteAndProcess } from '#/api/mes/pro/route/process'; |
| | | import { getWorkOrder } from '#/api/mes/pro/workorder'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useFormSchema } from '../data'; |
| | |
| | | } |
| | | } |
| | | |
| | | const batchDialogRef = ref<InstanceType<typeof BatchInputDialog>>(); |
| | | const batchActionType = ref<'submit' | 'approve'>(''); |
| | | |
| | | /** æ§è¡æäº¤ï¼å«æ¹æ¬¡å±æ§ï¼ */ |
| | | async function doSubmit(batchFields?: Record<string, unknown>) { |
| | | if (!formData.value?.id) return; |
| | | modalApi.lock(); |
| | | try { |
| | | await submitFeedback({ id: formData.value.id, ...batchFields }); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success('æ¥å·¥åå·²æäº¤'); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | } |
| | | |
| | | /** æäº¤ï¼ä»
å½è¡¨åè¾å¿«ç
§æåæ´æ¶æå
ä¿åï¼åè°ç¨æäº¤æ¥å£ */ |
| | | async function handleSubmit() { |
| | | const { valid } = await formApi.validate(); |
| | |
| | | await updateFeedback(data); |
| | | originalSnapshot.value = JSON.stringify(data); |
| | | } |
| | | await submitFeedback(formData.value!.id!); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success('æ¥è®¢åå·²æäº¤'); |
| | | |
| | | // æ£æ¥æ¯å¦éè¦å®¡æ¹ï¼è¥éè¦å®¡æ¹ï¼åä¸éè¦å¨æ¤é¶æ®µå¡«åæ¹æ¬¡å±æ§ï¼ç´æ¥æäº¤ |
| | | let feedbackApprove = false; |
| | | if (formData.value?.workOrderId) { |
| | | try { |
| | | const workOrder = await getWorkOrder(formData.value.workOrderId); |
| | | feedbackApprove = workOrder?.feedbackApprove ?? false; |
| | | } catch { |
| | | // ignore |
| | | } |
| | | } |
| | | |
| | | if (feedbackApprove) { |
| | | await doSubmit(); |
| | | return; |
| | | } |
| | | |
| | | let checkResult; |
| | | try { |
| | | checkResult = await getBatchRequirements(formData.value!.id!); |
| | | } catch { |
| | | // ignore |
| | | } |
| | | |
| | | if (checkResult?.needBatchInput) { |
| | | batchActionType.value = 'submit'; |
| | | batchDialogRef.value?.open({ checkResult }); |
| | | } else { |
| | | await doSubmit(); |
| | | } |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | } |
| | | |
| | | const batchDialogRef = ref<InstanceType<typeof BatchInputDialog>>(); |
| | | |
| | | /** æ§è¡å®¡æ¹ï¼å«æ¹æ¬¡å±æ§ï¼ */ |
| | | async function doApprove(batchFields?: Record<string, unknown>) { |
| | |
| | | const checkResult = await getBatchRequirements(formData.value.id); |
| | | if (checkResult?.needBatchInput) { |
| | | // éè¦å¡«åæ¹æ¬¡å±æ§ï¼å¼¹çªæ¶é |
| | | batchActionType.value = 'approve'; |
| | | batchDialogRef.value?.open({ checkResult }); |
| | | } else { |
| | | await doApprove(); |
| | |
| | | |
| | | /** æ¹æ¬¡å¼¹çªç¡®è®¤åè° */ |
| | | function handleBatchConfirm(batchData: Record<string, unknown>) { |
| | | doApprove(batchData); |
| | | if (batchActionType.value === 'submit') { |
| | | doSubmit(batchData); |
| | | } else { |
| | | doApprove(batchData); |
| | | } |
| | | } |
| | | |
| | | /** 审æ¹ä¸éè¿ */ |
| | |
| | | } |
| | | |
| | | /** çäº§ä»»å¡æ°å¢/ä¿®æ¹ç表å */ |
| | | export function useTaskFormSchema(formApi?: VbenFormApi, workOrderQuantity?: Ref<number | undefined>): VbenFormSchema[] { |
| | | export function useTaskFormSchema(formApi?: VbenFormApi, workOrderQuantity?: Ref<number | undefined>, feedbackRole?: Ref<string | undefined>): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'workstationId', |
| | |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©æ¥å·¥äºº', |
| | | api: getSimpleUserListByRoleCode, |
| | | params: { code: 'production_submit_work' }, |
| | | params: { code: feedbackRole?.value || 'production_submit_work' }, |
| | | labelField: 'nickname', |
| | | valueField: 'id', |
| | | mode: 'multiple', |
| | |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { createTask, getTask, updateTask } from '#/api/mes/pro/task'; |
| | | import { getWorkOrder } from '#/api/mes/pro/workorder'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useTaskFormSchema } from '../data'; |
| | |
| | | routeId?: number; |
| | | workOrderId?: number; |
| | | }>({}); // æ°å¢ä»»å¡æ¶ç订å / å·¥åºä¸ä¸æ |
| | | const feedbackRole = ref<string>(); |
| | | |
| | | const getTitle = computed(() => { |
| | | return formData.value?.id |
| | |
| | | context.value = {}; |
| | | return; |
| | | } |
| | | formApi.setState({ schema: useTaskFormSchema(formApi, workOrderQuantity) }); |
| | | // å è½½æ°æ® |
| | | const data = modalApi.getData<{ |
| | | colorCode?: string; |
| | | id?: number; |
| | |
| | | routeId?: number; |
| | | workOrderId?: number; |
| | | }>(); |
| | | |
| | | let wid: number | undefined; |
| | | |
| | | if (data?.id) { |
| | | modalApi.lock(); |
| | | try { |
| | | formData.value = await getTask(data.id); |
| | | wid = formData.value.workOrderId; |
| | | // è®¾ç½®å° values |
| | | await formApi.setValues(formData.value); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | return; |
| | | } else { |
| | | context.value = { |
| | | colorCode: data.colorCode, |
| | | itemId: data.itemId, |
| | | processId: data.processId, |
| | | routeId: data.routeId, |
| | | workOrderId: data.workOrderId, |
| | | }; |
| | | wid = data.workOrderId; |
| | | workOrderQuantity.value = data.quantity; |
| | | await formApi.setValues({ |
| | | colorCode: data.colorCode || '#00AEF3', |
| | | duration: 1, |
| | | quantity: data.quantity, |
| | | }); |
| | | } |
| | | context.value = { |
| | | colorCode: data.colorCode, |
| | | itemId: data.itemId, |
| | | processId: data.processId, |
| | | routeId: data.routeId, |
| | | workOrderId: data.workOrderId, |
| | | }; |
| | | workOrderQuantity.value = data.quantity; |
| | | await formApi.setValues({ |
| | | colorCode: data.colorCode || '#00AEF3', |
| | | duration: 1, |
| | | quantity: data.quantity, |
| | | }); |
| | | |
| | | if (wid) { |
| | | try { |
| | | const wo = await getWorkOrder(wid); |
| | | feedbackRole.value = wo.feedbackRole; |
| | | } catch { |
| | | // ignore |
| | | } |
| | | } |
| | | |
| | | formApi.setState({ schema: useTaskFormSchema(formApi, workOrderQuantity, feedbackRole) }); |
| | | }, |
| | | }); |
| | | </script> |
| | |
| | | import type { VbenFormApi, VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder'; |
| | | import { type MesProWorkOrderApi, getWorkOrderApproveProcessList } from '#/api/mes/pro/workorder'; |
| | | import { getSimpleRoleList } from '#/api/system/role'; |
| | | import type { MesProWorkOrderBomApi } from '#/api/mes/pro/workorder/bom'; |
| | | import type { MesProWorkOrderProcessApi } from '#/api/mes/pro/workorder/process'; |
| | | |
| | |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'feedbackRole', |
| | | label: 'æ¥å·¥è§è²', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | api: getSimpleRoleList, |
| | | disabled: headerReadonly, |
| | | labelField: 'name', |
| | | valueField: 'code', |
| | | mode: 'multiple', |
| | | placeholder: 'è¯·éæ©æ¥å·¥è§è²', |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'feedbackApprove', |
| | | label: 'æ¯å¦æ¥å·¥å®¡æ¹', |
| | | component: 'Switch', |
| | | componentProps: { |
| | | class: '!w-auto', |
| | | checkedChildren: 'æ¯', |
| | | unCheckedChildren: 'å¦', |
| | | disabled: headerReadonly, |
| | | }, |
| | | defaultValue: false, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'processDefinitionKey', |
| | | label: 'å®¡æ¹æµç¨', |
| | | component: 'ApiSelect', |
| | | componentProps: { |
| | | api: getWorkOrderApproveProcessList, |
| | | disabled: headerReadonly, |
| | | labelField: 'name', |
| | | valueField: 'key', |
| | | placeholder: 'è¯·éæ©å®¡æ¹æµç¨', |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['feedbackApprove'], |
| | | show: (values) => !!values.feedbackApprove, |
| | | rules(values) { |
| | | if (values.feedbackApprove) { |
| | | return 'selectRequired'; |
| | | } |
| | | return undefined; |
| | | }, |
| | | }, |
| | | }, |
| | | |
| | | { |
| | | fieldName: 'remark', |
| | | label: '夿³¨', |
| | | component: 'Textarea', |
| | |
| | | if (!valid || !formData.value?.id) return; |
| | | modalApi.lock(); |
| | | try { |
| | | const data = (await formApi.getValues()) as MesProWorkOrderApi.WorkOrder; |
| | | const data = (await formApi.getValues()) as any; |
| | | if (Array.isArray(data.feedbackRole)) { |
| | | data.feedbackRole = data.feedbackRole.join(','); |
| | | } |
| | | await updateWorkOrder({ ...formData.value, ...data }); |
| | | if (tempProcessList.value.length > 0) { |
| | | const processList = tempProcessList.value.map(item => ({ |
| | |
| | | if (isEditable.value) { |
| | | const current = JSON.stringify(await formApi.getValues()); |
| | | if (current !== originalSnapshot.value) { |
| | | const data = (await formApi.getValues()) as MesProWorkOrderApi.WorkOrder; |
| | | const data = (await formApi.getValues()) as any; |
| | | if (Array.isArray(data.feedbackRole)) { |
| | | data.feedbackRole = data.feedbackRole.join(','); |
| | | } |
| | | await updateWorkOrder({ ...formData.value, ...data }); |
| | | originalSnapshot.value = current; |
| | | } |
| | |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) return; |
| | | modalApi.lock(); |
| | | const data = (await formApi.getValues()) as MesProWorkOrderApi.WorkOrder; |
| | | const data = (await formApi.getValues()) as any; |
| | | if (Array.isArray(data.feedbackRole)) { |
| | | data.feedbackRole = data.feedbackRole.join(','); |
| | | } |
| | | try { |
| | | if (formData.value?.id) { |
| | | // ç¼è¾æ¨¡å¼ |
| | |
| | | modalApi.lock(); |
| | | try { |
| | | formData.value = await getWorkOrder(data.id); |
| | | await formApi.setValues(formData.value); |
| | | const formValues: any = { ...formData.value }; |
| | | if (formValues.feedbackRole && typeof formValues.feedbackRole === 'string') { |
| | | formValues.feedbackRole = formValues.feedbackRole.split(','); |
| | | } |
| | | await formApi.setValues(formValues); |
| | | currentProductId.value = formData.value.productId; |
| | | } finally { |
| | | modalApi.unlock(); |
| | |
| | | import type { MesProFeedbackApi } from '#/api/mes/pro/feedback'; |
| | | |
| | | import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'; |
| | | import { useRouter } from 'vue-router'; |
| | | |
| | | import { |
| | | DICT_TYPE, |
| | | MesProFeedbackStatusEnum, |
| | | } from '@vben/constants'; |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { useAccess } from '@vben/access'; |
| | | |
| | | import { Button, message, Popconfirm, Space } from 'ant-design-vue'; |
| | | |
| | |
| | | connectedComponent: FeedbackForm, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const { hasAccessByCodes } = useAccess(); |
| | | const router = useRouter(); |
| | | |
| | | const batchDialogRef = ref<InstanceType<typeof BatchInputDialog>>(); |
| | | const approvingRow = ref<MesProFeedbackApi.Feedback>(); |
| | |
| | | |
| | | /** æäº¤ï¼ä»
èç¨¿ï¼ */ |
| | | async function handleSubmit(row: MesProFeedbackApi.Feedback) { |
| | | await submitFeedback(row.id!); |
| | | await submitFeedback({ id: row.id! }); |
| | | message.success('æ¥å·¥åå·²æäº¤'); |
| | | refreshGrid(); |
| | | emit('reported'); |
| | |
| | | message.success('æ¥å·¥å已驳å'); |
| | | refreshGrid(); |
| | | emit('reported'); |
| | | } |
| | | |
| | | /** æ¥ç审æ¹è¯¦æ
*/ |
| | | function handleViewProcess(row: MesProFeedbackApi.Feedback) { |
| | | if (!row.processInstanceId) { |
| | | message.warning('该æ¥å·¥å°æªæäº¤å®¡æ¹'); |
| | | return; |
| | | } |
| | | router.push({ path: '/bpm/process-instance/detail', query: { id: row.processInstanceId } }); |
| | | } |
| | | |
| | | /** ç¶æå¸¸é便¨¡æ¿ä½¿ç¨ */ |
| | |
| | | å é¤ |
| | | </Button> |
| | | </Popconfirm> |
| | | <!-- 审æ¹ä¸ï¼å®¡æ¹éè¿ã驳å --> |
| | | <Popconfirm |
| | | <!-- 审æ¹ä¸ï¼æ¥çå®¡æ¹ --> |
| | | <Button |
| | | v-if="row.status === STATUS_APPROVING" |
| | | type="link" |
| | | size="small" |
| | | @click="handleViewProcess(row)" |
| | | > |
| | | æ¥çå®¡æ¹ |
| | | </Button> |
| | | <!-- |
| | | <Popconfirm |
| | | v-if="row.status === STATUS_APPROVING && hasAccessByCodes(['mes:pro-feedback:approve'])" |
| | | title="确认审æ¹éè¿è¯¥æ¥å·¥åï¼" |
| | | @confirm="handleApprove(row)" |
| | | > |
| | |
| | | </Button> |
| | | </Popconfirm> |
| | | <Popconfirm |
| | | v-if="row.status === STATUS_APPROVING" |
| | | v-if="row.status === STATUS_APPROVING && hasAccessByCodes(['mes:pro-feedback:approve'])" |
| | | title="确认驳å该æ¥å·¥åï¼" |
| | | @confirm="handleReject(row)" |
| | | > |
| | |
| | | 驳å |
| | | </Button> |
| | | </Popconfirm> |
| | | --> |
| | | </Space> |
| | | </template> |
| | | </Grid> |
| | |
| | | import { generateAutoCode } from '#/api/mes/md/autocode/record'; |
| | | import { |
| | | approveFeedback, |
| | | createFeedback, |
| | | createFeedback, getBatchRequirements, |
| | | getFeedback, |
| | | getFeedbackPage, |
| | | rejectFeedback, |
| | |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useFormSchema } from '../../../pro/feedback/data'; |
| | | |
| | | import BatchInputDialog from '../../../pro/feedback/modules/batch-input-dialog.vue'; |
| | | const batchDialogRef = ref<InstanceType<typeof BatchInputDialog>>(); |
| | | const batchActionType = ref<'submit' | 'approve'>(''); |
| | | const pendingSubmitMode = ref<'saveAndSubmit' | 'submit'>('submit'); |
| | | defineOptions({ name: 'WorkbenchFeedbackTab' }); |
| | | |
| | | const props = defineProps<{ |
| | |
| | | itemSpecification: task.itemSpecification, |
| | | unitMeasureName: task.unitMeasureName, |
| | | scheduledQuantity: task.quantity, |
| | | feedbackApprove: task.feedbackApprove, |
| | | checkFlag, |
| | | }); |
| | | |
| | | originalSnapshot.value = JSON.stringify(await getAlignedData()); |
| | | } |
| | | |
| | |
| | | if (props.task) { |
| | | await resetForTask(props.task); |
| | | } |
| | | |
| | | emit('reported'); |
| | | } |
| | | |
| | |
| | | return; |
| | | } |
| | | |
| | | pendingSubmitMode.value = 'saveAndSubmit'; |
| | | |
| | | // 4. è°ç¨æäº¤æ¥å£ |
| | | await submitFeedback(record.id!); |
| | | const isDone = await handleOnSubmit(feedbackId.value!,props.task.feedbackApprove); |
| | | if (!isDone) return; |
| | | |
| | | feedbackId.value = record.id; |
| | | feedbackStatus.value = record.status; |
| | | |
| | |
| | | await updateFeedback(data); |
| | | originalSnapshot.value = JSON.stringify(data); |
| | | } |
| | | await submitFeedback(feedbackId.value!); |
| | | message.success('æ¥è®¢åå·²æäº¤'); |
| | | |
| | | pendingSubmitMode.value = 'submit'; |
| | | const isDone = await handleOnSubmit(feedbackId.value!,props.task.feedbackApprove); |
| | | if (!isDone) return; |
| | | |
| | | message.success('æ¥å·¥åå·²æäº¤'); |
| | | // é置表å为æ°å»ºæ¨¡å¼ |
| | | if (props.task) { |
| | | await resetForTask(props.task); |
| | | } |
| | | emit('reported'); |
| | | } |
| | | |
| | | |
| | | /** æäº¤æ¹æ³ */ |
| | | async function handleOnSubmit(id:any,feedbackRate:any): Promise<boolean> { |
| | | if (feedbackRate&&feedbackRate==true){ |
| | | await submitFeedback({ id: feedbackId.value! }); |
| | | return true; |
| | | } |
| | | |
| | | const checkResult = await getBatchRequirements(id); |
| | | if (checkResult?.needBatchInput) { |
| | | batchActionType.value = 'submit'; |
| | | batchDialogRef.value?.open({ checkResult }); |
| | | return false; |
| | | } else { |
| | | await submitFeedback({ id: feedbackId.value! }); |
| | | return true; |
| | | } |
| | | } |
| | | |
| | | /** æ¹æ¬¡ç¡®è®¤åè° */ |
| | | async function handleBatchConfirm(batchData: Record<string, unknown>) { |
| | | if (batchActionType.value === 'submit') { |
| | | await submitFeedback({ id: feedbackId.value!, ...batchData }); |
| | | |
| | | if (pendingSubmitMode.value === 'saveAndSubmit') { |
| | | const data = await getAlignedData(); |
| | | const code = data.code as string; |
| | | Modal.success({ |
| | | title: 'ä¿åå¹¶æäº¤æå', |
| | | content: `æ¥å·¥å ${code} å·²æäº¤`, |
| | | centered: true, |
| | | }); |
| | | } else { |
| | | message.success('æ¥å·¥åå·²æäº¤'); |
| | | } |
| | | |
| | | if (props.task) { |
| | | await resetForTask(props.task); |
| | | } |
| | | emit('reported'); |
| | | } |
| | | } |
| | | |
| | | /** 审æ¹éè¿ */ |
| | |
| | | |
| | | <template> |
| | | <div class="feedback-tab"> |
| | | <BatchInputDialog ref="batchDialogRef" @confirm="handleBatchConfirm" /> |
| | | <div v-if="!task" class="feedback-tab__empty"> |
| | | 请ä»å·¦ä¾§å·¥ååè¡¨éæ©ä¸æ¡ä»»å¡ |
| | | </div> |
| | |
| | | } from '#/api/mes/pro/workrecord'; |
| | | import { getPendingFeedbackPage } from '#/api/mes/pro/task'; |
| | | import { router } from '#/router'; |
| | | import { useAccess } from '@vben/access'; |
| | | |
| | | import TaskListPanel from './components/TaskListPanel.vue'; |
| | | import WorkstationPanel from './components/WorkstationPanel.vue'; |
| | |
| | | // --- Tab é
ç½® --- |
| | | const tabItems: WorkbenchTabItem[] = [ |
| | | { key: 'detail', label: 'å·¥å详æ
', icon: 'lucide:file-text' }, |
| | | { key: 'feedback', label: 'æ¥å·¥', icon: 'lucide:edit-3' }, |
| | | { key: 'history', label: 'æ¥å·¥è®°å½', icon: 'lucide:history' }, |
| | | { key: 'feedback', label: 'æ¥å·¥', icon: 'lucide:edit-3', auth: ['mes:pro-feedback:create'] }, |
| | | { key: 'history', label: 'æ¥å·¥è®°å½', icon: 'lucide:history' ,auth: ['mes:pro-feedback:query']}, |
| | | { key: 'record-param', label: 'ç产记å½åæ°', icon: 'lucide:file-cog' }, |
| | | { key: 'product-issue', label: '颿', icon: 'lucide:clipboard-list' }, |
| | | { key: 'return-issue', label: 'éæ', icon: 'lucide:rotate-ccw' }, |
| | | ]; |
| | | |
| | | const { hasAccessByCodes } = useAccess(); |
| | | |
| | | const availableTabs = computed(() => { |
| | | return tabItems.filter((tab) => { |
| | | if (!tab.auth || tab.auth.length === 0) return true; |
| | | return hasAccessByCodes(tab.auth); |
| | | }); |
| | | }); |
| | | |
| | | const activeTab = ref<WorkbenchTabKey>('detail'); |
| | | const activeTabLabel = computed( |
| | | () => tabItems.find((t) => t.key === activeTab.value)?.label ?? '', |
| | | () => availableTabs.value.find((t) => t.key === activeTab.value)?.label ?? '', |
| | | ); |
| | | |
| | | // å½å¯ç¨ tab ååæ¶ï¼å¦æå½åéä¸ç tab ä¸å¯ç¨ï¼åèªå¨éæ©ç¬¬ä¸ä¸ªå¯ç¨ç tab |
| | | watch( |
| | | availableTabs, |
| | | (tabs) => { |
| | | if (tabs.length > 0 && !tabs.find((t) => t.key === activeTab.value)) { |
| | | activeTab.value = tabs[0]!.key; |
| | | } |
| | | }, |
| | | { immediate: true }, |
| | | ); |
| | | |
| | | // --- å·¥ä½ç« --- |
| | |
| | | |
| | | <nav class="workbench-nav"> |
| | | <button |
| | | v-for="tab in tabItems" |
| | | v-for="tab in availableTabs" |
| | | :key="tab.key" |
| | | type="button" |
| | | class="workbench-nav__item" |
| | |
| | | key: WorkbenchTabKey; |
| | | label: string; |
| | | icon: string; |
| | | auth?: string[]; |
| | | } |
| | | |
| | | /** å¾
æ¥å·¥ä»»å¡ï¼ä» API ç±»åé导åºï¼ä¾¿äºç»ä»¶å¼ç¨ï¼ */ |