feat(feedback): 优化报工审批流程并增加报工人校验
- 增加报工人身份校验,防止非指定人员提交报工
- 引入行锁机制解决报工审批与驳回时的并发问题
- 优化检验标识获取逻辑,优先使用工单工序快照
- 完善联产出物料检验逻辑,独立检验且不计入主任务进度
- 修复物料更新时因未回传来源标识导致的名称唯一性误判
- 修复设备绑定工作站查询时因多条记录引发的异常
- 补充生产、质检及仓储等模块的详细业务错误码
- 更新报工服务单元测试并补充相关依赖的 Mock 配置
| | |
| | | itemId?: number; |
| | | itemCode?: string; |
| | | itemName?: string; |
| | | quantity?: number; // é
æ¯ï¼æ¯ 1 个主产åºçé带æ°éï¼ä¸»äº§åºåºå®ä¸º 1 |
| | | mainFlag?: boolean; // æ¯å¦ä¸»äº§åº |
| | | sort?: number; |
| | | remark?: string; |
| | | } |
| | |
| | | name?: string; |
| | | checkFlag?: boolean; |
| | | createTime?: string[]; |
| | | onlyMine?: boolean; // ä»
æ¥çææ¯æ¥å·¥äººçä»»å¡ï¼å·¥ä½å°ä½¿ç¨ï¼ |
| | | } |
| | | } |
| | | |
| | |
| | | ); |
| | | } |
| | | |
| | | export interface BatchScheduleRequest { |
| | | workOrderIds: number[]; |
| | | startTime: string; |
| | | previewHash?: string; |
| | | previewTime?: string; |
| | | allowLate?: boolean; |
| | | } |
| | | |
| | | export interface BatchSchedulePreview { |
| | | canExecute: boolean; |
| | | previewHash: string; |
| | | previewTime: string; |
| | | warnings: string[]; |
| | | notices: string[]; |
| | | orders: { |
| | | workOrderId: number; |
| | | workOrderCode?: string; |
| | | workOrderName?: string; |
| | | quantity?: number; |
| | | taskCount: number; |
| | | requestDate?: string; |
| | | endTime?: string; |
| | | late: boolean; |
| | | error?: string; |
| | | }[]; |
| | | tasks: { |
| | | workOrderId: number; |
| | | workOrderCode: string; |
| | | processId: number; |
| | | processName: string; |
| | | workstationId: number; |
| | | workstationName: string; |
| | | machineryNames: string; |
| | | quantity: number; |
| | | hourlyCapacity: number; |
| | | duration: number; |
| | | startTime: string; |
| | | endTime: string; |
| | | }[]; |
| | | } |
| | | |
| | | export function previewBatchSchedule(data: BatchScheduleRequest) { |
| | | return requestClient.post<BatchSchedulePreview>('/mes/pro/task/auto-schedule/preview', data); |
| | | } |
| | | |
| | | export function executeBatchSchedule(data: BatchScheduleRequest) { |
| | | return requestClient.post<{ taskIds: number[] }>('/mes/pro/task/auto-schedule', data); |
| | | } |
| | | |
| | | /** æ¥è¯¢å¾
æ¥å·¥å°è´¦ï¼åé¡µï¼ */ |
| | | export function getPendingFeedbackPage( |
| | | params: MesProTaskApi.PendingFeedbackPageParams, |
| | |
| | | } |
| | | |
| | | /** æå·¥ä½ç«æ¥è¯¢å¾
æ¥å·¥ä»»å¡æ°ï¼key 为工ä½ç«ç¼å·ï¼ */ |
| | | export function getPendingFeedbackCountByWorkstation() { |
| | | export function getPendingFeedbackCountByWorkstation(onlyMine?: boolean) { |
| | | return requestClient.get<Record<string, number>>( |
| | | '/mes/pro/task/pending-count-by-workstation', |
| | | { params: onlyMine === undefined ? undefined : { onlyMine } }, |
| | | ); |
| | | } |
| | | |
| | |
| | | // 客æ·ï¼OQC/RQC åºæ¯ï¼ |
| | | clientId?: number; // å®¢æ· ID |
| | | clientName?: string; // 客æ·åç§° |
| | | canInspect?: boolean; // æ¯å¦å¯æ£éªï¼å·²é
ç½®å¯ç¨çæ£éªæ¨¡æ¿ï¼ |
| | | recordTime?: number; // è®°å½æ¶é´ï¼epoch msï¼ |
| | | } |
| | | } |
| | |
| | | name?: string; // å
¥åºååç§° |
| | | workOrderId?: number; // ç产订åç¼å· |
| | | workOrderCode?: string; // ç产订åç¼ç |
| | | produceId?: number; // æ¥æºç产产åºåç¼å·ï¼ä¸ºç©ºè¡¨ç¤ºæå·¥å
¥åºå |
| | | itemId?: number; // 产åç©æç¼å· |
| | | itemCode?: string; // 产åç©æç¼ç |
| | | itemName?: string; // 产åç©æåç§° |
| | |
| | | <script lang="ts"> |
| | | // 模å级æ è®°ï¼æµè§å¨å·æ°ä¼éç½®ï¼å¸å±éæ°æè½½ï¼å¦ä»å
¨å±å·¥ä½å°è¿åï¼ä¸ä¼éç½® |
| | | let tabbarInitialized = false; |
| | | </script> |
| | | |
| | | <script lang="ts" setup> |
| | | import { computed } from 'vue'; |
| | | import { useRoute } from 'vue-router'; |
| | |
| | | }); |
| | | }); |
| | | |
| | | // å·æ°å妿ä¸ä¿ætabç¶æï¼å
³éå
¶ä»tab |
| | | if (!preferences.tabbar.persist) { |
| | | // å·æ°å妿ä¸ä¿ætabç¶æï¼å
³éå
¶ä»tabï¼ |
| | | // ä»
卿µè§å¨å·æ°ï¼æ¨¡åéæ°å è½½ï¼æ¶æ§è¡ä¸æ¬¡ï¼å¸å±éæ°æè½½ä¸æ¸
ç©ºé¡µç¾ |
| | | if (!preferences.tabbar.persist && !tabbarInitialized) { |
| | | tabbarInitialized = true; |
| | | tabbarStore.closeOtherTabs(route); |
| | | } |
| | | </script> |
| | |
| | | |
| | | const emit = defineEmits<{ |
| | | selected: [row: MesMdProductBomApi.ProductBom]; |
| | | 'selected-many': [rows: MesMdProductBomApi.ProductBom[]]; |
| | | }>(); |
| | | |
| | | const open = ref(false); // å¼¹çªæ¯å¦æå¼ |
| | | const list = ref<MesMdProductBomApi.ProductBom[]>([]); // BOM åç©æå表 |
| | | const selectedRow = ref<MesMdProductBomApi.ProductBom>(); // å½åéä¸ç BOM è¡ |
| | | const multiple = ref(false); // æ¯å¦å¤é |
| | | const disabledItemIds = ref<number[]>([]); // 已添å ãç¦æ¢éå¤éæ©çç©æ |
| | | |
| | | const baseColumns = useProductBomGridColumns(true) || []; |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | gridOptions: { |
| | | border: true, |
| | | columns: [ |
| | | { type: 'radio', width: 55, align: 'center' }, |
| | | ...(useProductBomGridColumns(true) || []), |
| | | ...baseColumns, |
| | | ], |
| | | checkboxConfig: { |
| | | highlight: true, |
| | | checkMethod: ({ row }: { row: MesMdProductBomApi.ProductBom }) => |
| | | !disabledItemIds.value.includes(row.bomItemId!), |
| | | }, |
| | | data: list.value, |
| | | height: 500, |
| | | keepSource: true, |
| | |
| | | } as VxeTableGridOptions<MesMdProductBomApi.ProductBom>, |
| | | gridEvents: { |
| | | cellDblclick: ({ row }: { row: MesMdProductBomApi.ProductBom }) => { |
| | | if (multiple.value || disabledItemIds.value.includes(row.bomItemId!)) { |
| | | return; |
| | | } |
| | | selectedRow.value = row; |
| | | gridApi.grid.setRadioRow(row); |
| | | handleConfirm(); |
| | |
| | | }, |
| | | }); |
| | | |
| | | /** 忢åé/å¤éå */ |
| | | function applySelectMode() { |
| | | gridApi.setGridOptions({ |
| | | columns: [ |
| | | multiple.value |
| | | ? { type: 'checkbox', width: 55, align: 'center' } |
| | | : { type: 'radio', width: 55, align: 'center' }, |
| | | ...baseColumns, |
| | | ], |
| | | }); |
| | | } |
| | | |
| | | /** æå¼ BOM ç©æéæ©å¼¹çª */ |
| | | async function openModal(itemId: number, selectedBomItemId?: number) { |
| | | async function openModal( |
| | | itemId: number, |
| | | selectedBomItemId?: number, |
| | | options?: { disabledItemIds?: number[]; multiple?: boolean }, |
| | | ) { |
| | | open.value = true; |
| | | selectedRow.value = undefined; |
| | | multiple.value = options?.multiple === true; |
| | | disabledItemIds.value = options?.disabledItemIds ?? []; |
| | | applySelectMode(); |
| | | gridApi.setLoading(true); |
| | | try { |
| | | list.value = await getProductBomListByItemId(itemId); |
| | | gridApi.setGridOptions({ data: list.value }); |
| | | await nextTick(); |
| | | if (selectedBomItemId !== null) { |
| | | if (!multiple.value && selectedBomItemId !== null) { |
| | | const match = list.value.find( |
| | | (row) => row.bomItemId === selectedBomItemId, |
| | | ); |
| | |
| | | open.value = false; |
| | | selectedRow.value = undefined; |
| | | await gridApi.grid.clearRadioRow(); |
| | | await gridApi.grid.clearCheckboxRow(); |
| | | } |
| | | |
| | | /** ç¡®è®¤éæ© BOM ç©æ */ |
| | | function handleConfirm() { |
| | | if (multiple.value) { |
| | | const rows = gridApi.grid.getCheckboxRecords(); |
| | | if (!rows.length) { |
| | | message.warning('è¯·éæ©è³å°ä¸æ¡æ°æ®'); |
| | | return; |
| | | } |
| | | emit('selected-many', rows); |
| | | open.value = false; |
| | | return; |
| | | } |
| | | if (!selectedRow.value) { |
| | | message.warning('è¯·éæ©ä¸æ¡æ°æ®'); |
| | | return; |
| | |
| | | workstationId: task.workstationId, |
| | | reportableQuantity: task.reportableQuantity, |
| | | }); |
| | | // å·¥èºè·¯çº¿å·¥åºç checkFlag å³å®æ°éåºåå±ç¤º |
| | | if (task.routeId && task.processId) { |
| | | // æ¯å¦è´¨æ£ä»¥ä»»å¡ï¼å·¥åå·¥åºå¿«ç
§ï¼ä¸ºåï¼ä»
å¨ç¼ºå¤±æ¶åéå·¥èºè·¯çº¿å·¥åº |
| | | if (typeof task.checkFlag === 'boolean') { |
| | | await formApi?.setFieldValue('checkFlag', task.checkFlag); |
| | | } else if (task.routeId && task.processId) { |
| | | try { |
| | | const routeProcess = await getRouteProcessByRouteAndProcess( |
| | | task.routeId, |
| | |
| | | <script lang="ts" setup> |
| | | <script lang="ts" setup> |
| | | import type { FormType } from '../data'; |
| | | |
| | | import type { MesProFeedbackApi } from '#/api/mes/pro/feedback'; |
| | |
| | | import { MesAutoCodeRuleCode, MesProFeedbackStatusEnum } from '@vben/constants'; |
| | | import { useUserStore } from '@vben/stores'; |
| | | |
| | | import { Button, message, Popconfirm, Tabs } from 'ant-design-vue'; |
| | | import { Alert, Button, message, Popconfirm, Tabs } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { generateAutoCode } from '#/api/mes/md/autocode/record'; |
| | |
| | | } |
| | | |
| | | /** å 载工åºç checkFlag ç¨äºåæ¾æ°éåºå */ |
| | | async function resolveCheckFlag(routeId?: number, processId?: number) { |
| | | async function resolveCheckFlag( |
| | | routeId?: number, |
| | | processId?: number, |
| | | taskCheckFlag?: boolean, |
| | | ) { |
| | | // æ¯å¦è´¨æ£ä»¥ä»»å¡ï¼å·¥åå·¥åºå¿«ç
§ï¼ä¸ºå |
| | | if (typeof taskCheckFlag === 'boolean') { |
| | | return taskCheckFlag; |
| | | } |
| | | if (!routeId || !processId) { |
| | | return true; |
| | | } |
| | |
| | | const checkFlag = await resolveCheckFlag( |
| | | formData.value.routeId, |
| | | formData.value.processId, |
| | | formData.value.checkFlag, |
| | | ); |
| | | // è®¾ç½®å° values |
| | | await formApi.setValues({ ...formData.value, checkFlag }); |
| | |
| | | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-3/5"> |
| | | <Alert |
| | | v-if="canApprove" |
| | | class="mx-4 mb-4" |
| | | type="info" |
| | | show-icon |
| | | message="审æ¹åè¯·æ ¸å¯¹ç产ä¸åºåé
ç½®" |
| | | description="èªå¨æ£æå·¥åºéè¦çº¿è¾¹åºæè¶³å¤ç©æï¼éæ£å·¥åºéè¦é
ç½®è¿ç¨æ£éªæ¹æ¡ãæ«éå·¥åºè¯·å
é
ç½®æåå
¥åºä»ä½ã" |
| | | /> |
| | | <Form class="mx-4" /> |
| | | <FeedbackParamFields ref="paramFields" :task-id="paramTaskId" :snapshot="paramSnapshot" :readonly="!isEditable" /> |
| | | <Tabs |
| | |
| | | |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { Button, Select } from 'ant-design-vue'; |
| | | import { Button, Checkbox, InputNumber, Select, Tooltip } from 'ant-design-vue'; |
| | | import { MdItemSelect } from '#/views/mes/md/item/components'; |
| | | |
| | | defineOptions({ name: 'RouteOutputAttribution' }); |
| | | |
| | |
| | | |
| | | /** éæ©äº§åºç©ææ¶ï¼é»è®¤å½å±å°èªèº«äº§åï¼åæåå
±ç¨æ¶åæå¨æ¹æâéç¨âï¼ */ |
| | | function handleItemChange(index: number, itemId: number | undefined) { |
| | | const current = rows.value[index]; |
| | | const patch: Partial<MesProRouteProcessApi.OutputItem> = { itemId }; |
| | | if (current?.productId == null && itemId != null) { |
| | | patch.productId = itemId; |
| | | } |
| | | patch.productId = productOptions.value.some((product) => product.value === itemId) ? itemId : undefined; |
| | | // åä¸å½å±åç»å
é»è®¤ç¬¬ä¸æ¡ä¸ºä¸»äº§åºï¼æ°å å
¥ç产åºè¥åç»å
è¿æ²¡æä¸»äº§åºåè®¾ä¸ºä¸»äº§åº |
| | | const groupKey = patch.productId ?? GENERAL_PRODUCT; |
| | | const groupHasMain = rows.value.some( |
| | | (row, i) => |
| | | i !== index && |
| | | row.itemId != null && |
| | | (row.productId ?? GENERAL_PRODUCT) === groupKey && |
| | | row.mainFlag === true, |
| | | ); |
| | | patch.mainFlag = !groupHasMain; |
| | | patch.quantity = groupHasMain ? (rows.value[index]?.quantity ?? 1) : 1; |
| | | updateRow(index, patch); |
| | | } |
| | | |
| | |
| | | }); |
| | | } |
| | | |
| | | /** åæ¢ä¸»äº§åºï¼åä¸å½å±åç»å
åªè½æä¸ä¸ªä¸»äº§åºï¼ä¸»äº§åºé
æ¯åºå®ä¸º 1 */ |
| | | function handleMainChange(index: number, checked: boolean) { |
| | | const target = rows.value[index]; |
| | | if (!target) { |
| | | return; |
| | | } |
| | | const groupKey = target.productId ?? GENERAL_PRODUCT; |
| | | commit( |
| | | rows.value.map((row, i) => { |
| | | if (i === index) { |
| | | return { |
| | | ...row, |
| | | mainFlag: checked, |
| | | quantity: checked ? 1 : (row.quantity ?? 1), |
| | | }; |
| | | } |
| | | if (checked && (row.productId ?? GENERAL_PRODUCT) === groupKey) { |
| | | return { ...row, mainFlag: false }; |
| | | } |
| | | return row; |
| | | }), |
| | | ); |
| | | } |
| | | |
| | | function handleQuantityChange(index: number, value: number | null) { |
| | | updateRow(index, { quantity: value ?? undefined }); |
| | | } |
| | | |
| | | function addRow() { |
| | | commit([...rows.value, { itemId: undefined, productId: undefined }]); |
| | | commit([ |
| | | ...rows.value, |
| | | { itemId: undefined, productId: undefined, quantity: 1 }, |
| | | ]); |
| | | } |
| | | |
| | | function removeRow(index: number) { |
| | |
| | | :key="index" |
| | | class="flex items-center gap-2" |
| | | > |
| | | <Select |
| | | :disabled="disabled" |
| | | :options="productOptions" |
| | | :placeholder="'éæ©äº§åºç©æ'" |
| | | :value="row.itemId" |
| | | class="min-w-0 flex-1" |
| | | option-filter-prop="label" |
| | | show-search |
| | | @change="(value) => handleItemChange(index, value as number | undefined)" |
| | | /> |
| | | <MdItemSelect :disabled="disabled" :model-value="row.itemId" placeholder="éæ©äº§åºç©æ" class="min-w-0 flex-1" @update:model-value="(value) => handleItemChange(index, value as number | undefined)" /> |
| | | <Select |
| | | :disabled="disabled || row.itemId == null" |
| | | :options="attributionOptions" |
| | |
| | | class="min-w-0 flex-1" |
| | | @change="(value) => handleAttributionChange(index, value as number | undefined)" |
| | | /> |
| | | <Tooltip |
| | | title="é
æ¯ï¼æ¯ 1 个主产åºçé带æ°éï¼ä¾å¦ä¸»äº§åº 1 å°é带 2 个åæååå¡« 2ï¼ä¸»äº§åºåºå®ä¸º 1" |
| | | placement="top" |
| | | > |
| | | <span class="inline-flex shrink-0"> |
| | | <InputNumber |
| | | :disabled="disabled || row.itemId == null || row.mainFlag === true" |
| | | :min="0.000001" |
| | | :precision="6" |
| | | :value="row.mainFlag === true ? 1 : (row.quantity ?? 1)" |
| | | class="w-32" |
| | | placeholder="é
æ¯" |
| | | addon-before="Ã" |
| | | @change="(value) => handleQuantityChange(index, value as number | null)" |
| | | /> |
| | | </span> |
| | | </Tooltip> |
| | | <Checkbox |
| | | :checked="row.mainFlag === true" |
| | | :disabled="disabled || row.itemId == null" |
| | | class="shrink-0" |
| | | @change="(event) => handleMainChange(index, event.target.checked)" |
| | | > |
| | | ä¸»äº§åº |
| | | </Checkbox> |
| | | <Button |
| | | :disabled="disabled" |
| | | danger |
| | |
| | | componentProps: { |
| | | products, |
| | | }, |
| | | help: 'é
æ¯è¡¨ç¤ºæ¯äº§åº 1 ä¸ªä¸»äº§åºæ¶çé带æ°éï¼ä¸»äº§åºåºå®ä¸º 1ï¼è产åæå®é
æ¯ä¾å¡«åï¼å¦ 1 å°ä¸»äº§åé带 2 个åæååå¡« 2ï¼ï¼æ¥å·¥æ¶æãæ¬æ¬¡æ¥å·¥æ°é à é
æ¯ãçæåäº§åºæ°é', |
| | | formItemClass: 'col-span-2', |
| | | }, |
| | | { |
| | |
| | | placeholder: 'è¯·éæ©', |
| | | }, |
| | | rules: z.number().default(3), |
| | | dependencies: { triggerFields: [''], show: () => false }, |
| | | }, |
| | | { |
| | | fieldName: 'colorCode', |
| | |
| | | return outputs |
| | | .map((output) => { |
| | | const name = output.itemName ?? output.itemCode ?? output.itemId; |
| | | return output.productId == null |
| | | ? `${name}ï¼éç¨ï¼` |
| | | : `${name}ï¼${output.productName ?? output.productId}ï¼`; |
| | | const ratio = output.quantity ?? 1; |
| | | const text = `${name} Ã ${ratio}`; |
| | | const product = |
| | | output.productId == null |
| | | ? 'éç¨' |
| | | : (output.productName ?? output.productId); |
| | | return `${output.mainFlag ? 'â
' : ''}${text}ï¼${product}ï¼`; |
| | | }) |
| | | .join('ã'); |
| | | }, |
| | | }, |
| | | { field: 'nextProcessName', title: 'ä¸ä¸éå·¥åº', width: 140 }, |
| | | { |
| | | field: 'linkType', |
| | | title: 'ä¸ä¸ä¸éå·¥åºå
³ç³»', |
| | | width: 160, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.MES_PRO_LINK_TYPE }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'keyFlag', |
| | | title: 'å
³é®å·¥åº', |
| | |
| | | <Alert |
| | | class="mx-4 mb-4" |
| | | message="æå®é
ç产èç¹é
置产åº" |
| | | description="ä¸é´å·¥åºæ²¡æç¬ç«äº§åºæ¶å¯ç空ï¼å½¢æåæåæ¶éæ©å¯¹åºåæåï¼æ«éå·¥åºå¿
须鿩å
¨é¨æåãå½å±è¡¨ç¤ºè¯¥äº§åºå±äºåªä¸ªäº§åï¼å¤äº§åå
±ç¨çåæåéâéç¨âï¼äº§åä¸å±ï¼å°¤å
¶æ¯æåï¼å½å±å°èªèº«äº§åãå¦å¼å¯æ¥å·¥èªå¨æ£æï¼æ¥å·¥æ¶ä¼æå·¥åºææé
ç½®æ£ååºåã" |
| | | description="ä¸é´å·¥åºæ²¡æç¬ç«äº§åºæ¶å¯ç空ï¼å½¢æåæåæ¶ä»åºç¡ç©æéæ©å¯¹åºåæåï¼æ«éå·¥åºå¿
须鿩å
¨é¨æåãå½å±è¡¨ç¤ºè¯¥äº§åºå±äºåªä¸ªäº§åï¼å¤äº§åå
±ç¨çåæåéâéç¨âï¼äº§åä¸å±ï¼å°¤å
¶æ¯æåï¼å½å±å°èªèº«äº§åãå¦å¼å¯æ¥å·¥èªå¨æ£æï¼æ¥å·¥æ¶ä¼æå·¥åºææé
ç½®æ£ååºåã" |
| | | show-icon |
| | | type="info" |
| | | /> |
| | |
| | | <script lang="ts" setup> |
| | | import type { FormType } from '../data'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { MesMdProductBomApi } from '#/api/mes/md/item/productBom'; |
| | | import type { MesProRouteProcessApi } from '#/api/mes/pro/route/process'; |
| | | import type { MesProRouteProductBomApi } from '#/api/mes/pro/route/productbom'; |
| | | |
| | |
| | | import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { getRouteProcessListByRoute } from '#/api/mes/pro/route/process'; |
| | | import { |
| | | createRouteProductBom, |
| | | deleteRouteProductBom, |
| | | getRouteProductBomList, |
| | | } from '#/api/mes/pro/route/productbom'; |
| | | import { $t } from '#/locales'; |
| | | import { MdProductBomSelectDialog } from '#/views/mes/md/item/components'; |
| | | |
| | | import { useRouteProductBomGridColumns } from '../data'; |
| | | import BomForm from './bom-form.vue'; |
| | |
| | | connectedComponent: BomForm, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const bomSelectRef = ref<InstanceType<typeof MdProductBomSelectDialog>>(); |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | gridOptions: { |
| | |
| | | } |
| | | } |
| | | |
| | | /** æ°å¢ BOM */ |
| | | /** æ°å¢ BOMï¼æå¼äº§å BOM ç©æå¼¹çªï¼æ¯æå¤éæ¹éæ·»å */ |
| | | function handleCreate() { |
| | | if (!activeProcessId.value) { |
| | | message.warning('请å
鿩工åº'); |
| | | return; |
| | | } |
| | | bomFormModalApi |
| | | .setData({ |
| | | processId: activeProcessId.value, |
| | | productId: props.productId, |
| | | routeId: props.routeId, |
| | | }) |
| | | .open(); |
| | | bomSelectRef.value?.open(props.productId, undefined, { |
| | | multiple: true, |
| | | disabledItemIds: list.value |
| | | .map((item) => item.itemId) |
| | | .filter((id): id is number => id !== undefined), |
| | | }); |
| | | } |
| | | |
| | | /** æ¹éæ·»å éä¸ç BOM ç©æ */ |
| | | async function handleSelectedMany(rows: MesMdProductBomApi.ProductBom[]) { |
| | | const processId = activeProcessId.value; |
| | | if (!processId) { |
| | | return; |
| | | } |
| | | gridApi.setLoading(true); |
| | | try { |
| | | for (const row of rows) { |
| | | await createRouteProductBom({ |
| | | routeId: props.routeId, |
| | | processId, |
| | | productId: props.productId, |
| | | itemId: row.bomItemId, |
| | | quantity: row.quantity ?? 1, |
| | | }); |
| | | } |
| | | message.success(`已添å ${rows.length} æ¡ææç©æ`); |
| | | await getList(); |
| | | emit('change'); |
| | | } finally { |
| | | gridApi.setLoading(false); |
| | | } |
| | | } |
| | | |
| | | /** ç¼è¾ BOM */ |
| | |
| | | |
| | | <template> |
| | | <BomFormModal @success="handleSaved" /> |
| | | <MdProductBomSelectDialog |
| | | ref="bomSelectRef" |
| | | @selected-many="handleSelectedMany" |
| | | /> |
| | | <Tabs v-model:active-key="activeProcessId" @change="getList"> |
| | | <TabPane |
| | | v-for="item in processList" |
| | |
| | | text: 'æ«éå·¥åºçæå产åºå¿
é¡»å½å±å°èªèº«äº§åï¼ä¸è½ä½¿ç¨âéç¨â', |
| | | }); |
| | | } |
| | | const productIds = new Set(productList.value.map((item) => item.itemId)); |
| | | const invalidOutputCount = processList.value.filter((process) => |
| | | process.outputItemIds?.some((itemId) => !productIds.has(itemId)), |
| | | ).length; |
| | | if (invalidOutputCount) { |
| | | issues.push({ |
| | | step: 2, |
| | | text: `${invalidOutputCount} éå·¥åºçäº§åºæªå
å«å¨è·¯çº¿äº§åºäº§åä¸`, |
| | | }); |
| | | } |
| | | const noBomProducts = productList.value.filter( |
| | | (item) => item.itemId && !configuredProductIds.value.has(item.itemId), |
| | | ); |
| | |
| | | <section v-if="routeId" v-show="activeStep === 1" class="route-step-panel"> |
| | | <Alert |
| | | class="mb-4" |
| | | message="å
ç»´æ¤ææå¯è½çå·¥åºäº§åº" |
| | | description="å®é
å½¢æçä¸é´åæååæ«éå·¥åºçæåé½è¦å å
¥ï¼å¹¶æå®å
¥åºä»ä½ï¼éæ©ä¸ä¸ªæåä½ä¸ºæ¨¡æ¿å¼ç¨ï¼ä¾å·¥èºæµç¨æ¨¡æ¿ä¸è½½ä½¿ç¨ã" |
| | | message="ç»´æ¤æå忥工å
¥åºä½ç½®" |
| | | description="è¿éåªç»´æ¤æåå¹¶æå®å
¥åºä»ä½ï¼ä¸é´åæåå¨å·¥åºäº§åºä¸ä»åºç¡ç©æéæ©ãéæ©ä¸ä¸ªæåä½ä¸ºæ¨¡æ¿å¼ç¨ï¼ä¾å·¥èºæµç¨æ¨¡æ¿ä¸è½½ä½¿ç¨ã" |
| | | show-icon |
| | | type="info" |
| | | /> |
| | |
| | | /** å¾
æäº§è®¢åå表çåæ®µ */ |
| | | export function useGridColumns(): VxeTableGridOptions<MesProWorkOrderApi.WorkOrder>['columns'] { |
| | | return [ |
| | | { type: 'checkbox', width: 48, fixed: 'left' }, |
| | | { |
| | | field: 'code', |
| | | title: '订åç¼ç ', |
| | |
| | | import { useRouter } from 'vue-router'; |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | import { MesProWorkOrderStatusEnum } from '@vben/constants'; |
| | | import { MesProWorkOrderStatusEnum, MesProWorkOrderTypeEnum } from '@vben/constants'; |
| | | |
| | | import { Button, Card, message, Tabs } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { getGanttTaskList, previewAutoGenerate, autoGenerateTasks } from '#/api/mes/pro/task'; |
| | | import { getWorkOrderPage } from '#/api/mes/pro/workorder'; |
| | | import { getGanttTaskList } from '#/api/mes/pro/task'; |
| | | |
| | | import ArchiveDetail from '#/views/mes/pd/archive/modules/detail.vue'; |
| | | import { GanttChart } from './components'; |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import ScheduleForm from './modules/schedule-form.vue'; |
| | | import SchedulePreview from '../workorder-center/modules/schedule-preview.vue'; |
| | | import AutoSchedule from './modules/auto-schedule.vue'; |
| | | |
| | | const router = useRouter(); |
| | | const ganttTasks = ref<any[]>([]); // çç¹å¾é¢è§æ°æ® |
| | |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [SchedulePreviewModal, schedulePreviewModalApi] = useVbenModal({ |
| | | connectedComponent: SchedulePreview, |
| | | const [AutoScheduleModal, autoScheduleModalApi] = useVbenModal({ |
| | | connectedComponent: AutoSchedule, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | /** å·æ°è¡¨æ ¼ */ |
| | | function handleRefresh() { |
| | | gridApi.grid.clearCheckboxRow(); |
| | | gridApi.query(); |
| | | loadGanttPreview(); |
| | | } |
| | |
| | | scheduleModalApi.setData({ formType: 'schedule', id: row.id }).open(); |
| | | } |
| | | |
| | | /** ä¸é®æäº§é¢è§ */ |
| | | async function handleAutoSchedulePreview(row: MesProWorkOrderApi.WorkOrder) { |
| | | try { |
| | | const preview = await previewAutoGenerate({ workOrderId: row.id! }); |
| | | schedulePreviewModalApi |
| | | .setData({ workOrder: row, preview }) |
| | | .open(); |
| | | } catch { |
| | | message.error('è·åæäº§é¢è§å¤±è´¥'); |
| | | } |
| | | function canAutoSchedule(row: MesProWorkOrderApi.WorkOrder) { |
| | | return row.status === MesProWorkOrderStatusEnum.CONFIRMED |
| | | && row.type === MesProWorkOrderTypeEnum.SELF |
| | | && (row.quantity ?? 0) > 0 && (row.quantityProduced ?? 0) < (row.quantity ?? 0); |
| | | } |
| | | |
| | | /** ä¸é®æäº§æ§è¡ */ |
| | | async function handleAutoSchedule(row: MesProWorkOrderApi.WorkOrder) { |
| | | try { |
| | | const result = await autoGenerateTasks({ workOrderId: row.id! }); |
| | | message.success(`æäº§æåï¼çæ ${result.taskIds?.length || 0} 个任å¡`); |
| | | handleRefresh(); |
| | | } catch { |
| | | message.error('æäº§å¤±è´¥'); |
| | | } |
| | | function handleAutoSchedule(row?: MesProWorkOrderApi.WorkOrder) { |
| | | const rows: MesProWorkOrderApi.WorkOrder[] = row ? [row] : gridApi.grid.getCheckboxRecords(); |
| | | if (!rows.length) return message.warning('请å
å¾éå¾
æäº§å·¥å'); |
| | | if (rows.length > 30) return message.warning('䏿¬¡æå¤éæ© 30 个工åï¼è¯·åæ¹æäº§'); |
| | | if (rows.some((order) => !canAutoSchedule(order))) return message.warning('è¯·éæ©å·²ç¡®è®¤ä¸æªçäº§å®æçèªè¡ç产工å'); |
| | | autoScheduleModalApi.setData({ workOrderIds: rows.map((order) => order.id!) }).open(); |
| | | } |
| | | |
| | | /** æå¼å½æ¡£è¯¦æ
*/ |
| | |
| | | gridOptions: { |
| | | columns: useGridColumns(), |
| | | keepSource: true, |
| | | checkboxConfig: { checkStrictly: true, showHeader: true, checkMethod: ({ row }: { row: MesProWorkOrderApi.WorkOrder }) => canAutoSchedule(row) }, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => { |
| | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <ScheduleModal @success="handleRefresh" /> |
| | | <SchedulePreviewModal @success="handleRefresh" /> |
| | | <AutoScheduleModal @success="handleRefresh" /> |
| | | |
| | | <!-- æäº§çç¹å¾é¢è§ --> |
| | | <Card title="æäº§çç¹å¾" class="mb-4"> |
| | |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'ä¸é®æäº§', |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['mes:pro-task:create'], |
| | | onClick: () => handleAutoSchedule(), |
| | | }, |
| | | { |
| | | label: 'çç¹å¾ç¼è¾', |
| | | type: 'primary', |
| | | icon: ACTION_ICON.EDIT, |
| | |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'ä¸é®æäº§', |
| | | type: 'link', |
| | | auth: ['mes:pro-task:create'], |
| | | ifShow: canAutoSchedule(row), |
| | | onClick: () => handleAutoSchedule(row), |
| | | }, |
| | | { |
| | | label: 'æäº§', |
| | | type: 'link', |
| | | auth: ['mes:pro-task:create'], |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script setup lang="ts"> |
| | | import type { BatchSchedulePreview, BatchScheduleRequest } from '#/api/mes/pro/task'; |
| | | import { computed, ref, watch } from 'vue'; |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { Alert, Button, Checkbox, DatePicker, Descriptions, Table, Tag, message } from 'ant-design-vue'; |
| | | import dayjs from 'dayjs'; |
| | | import { executeBatchSchedule, previewBatchSchedule } from '#/api/mes/pro/task'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | const ids = ref<number[]>([]); |
| | | function defaultStartTime() { |
| | | let value = dayjs().add(5, 'minute').startOf('minute'); |
| | | if (value.hour() >= 18) value = value.add(1, 'day').hour(9).minute(0).second(0); |
| | | else if (value.hour() < 9) value = value.hour(9).minute(0).second(0); |
| | | while (value.day() === 0 || value.day() === 6) value = value.add(1, 'day').hour(9).minute(0).second(0); |
| | | return value; |
| | | } |
| | | const startTime = ref(defaultStartTime()); |
| | | const preview = ref<BatchSchedulePreview>(); |
| | | const loading = ref(false); |
| | | const executing = ref(false); |
| | | const acceptLate = ref(false); |
| | | const errorText = ref(''); |
| | | let sequence = 0; |
| | | let open = false; |
| | | const canConfirm = computed(() => !!preview.value?.canExecute && !loading.value && !executing.value |
| | | && (!preview.value.warnings.length || acceptLate.value)); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | confirmText: '确认æäº§', |
| | | confirmDisabled: true, |
| | | onBeforeClose: () => !executing.value, |
| | | onConfirm: execute, |
| | | async onOpenChange(isOpen) { |
| | | open = isOpen; |
| | | sequence++; |
| | | if (!isOpen) { |
| | | preview.value = undefined; |
| | | ids.value = []; |
| | | loading.value = false; |
| | | errorText.value = ''; |
| | | return; |
| | | } |
| | | ids.value = modalApi.getData<{ workOrderIds: number[] }>().workOrderIds; |
| | | startTime.value = defaultStartTime(); |
| | | await calculate(); |
| | | }, |
| | | }); |
| | | |
| | | watch(canConfirm, (value) => modalApi.setState({ confirmDisabled: !value })); |
| | | watch(startTime, () => { |
| | | sequence++; |
| | | preview.value = undefined; |
| | | acceptLate.value = false; |
| | | loading.value = false; |
| | | errorText.value = ''; |
| | | }, { flush: 'sync' }); |
| | | |
| | | function request(): BatchScheduleRequest { |
| | | return { workOrderIds: [...ids.value], startTime: startTime.value.format('YYYY-MM-DD HH:mm:ss') }; |
| | | } |
| | | |
| | | async function calculate() { |
| | | if (executing.value || !startTime.value || !ids.value.length) return; |
| | | const current = ++sequence; |
| | | preview.value = undefined; |
| | | acceptLate.value = false; |
| | | errorText.value = ''; |
| | | loading.value = true; |
| | | try { |
| | | const result = await previewBatchSchedule(request()); |
| | | if (open && current === sequence) preview.value = result; |
| | | } catch (error) { |
| | | if (open && current === sequence) errorText.value = error instanceof Error ? error.message : 'é¢è§å¤±è´¥ï¼è¯·ç¨åéè¯'; |
| | | } finally { |
| | | if (current === sequence) loading.value = false; |
| | | } |
| | | } |
| | | |
| | | async function execute() { |
| | | if (!canConfirm.value || !preview.value) return; |
| | | executing.value = true; |
| | | errorText.value = ''; |
| | | modalApi.lock(); |
| | | try { |
| | | const result = await executeBatchSchedule({ ...request(), previewHash: preview.value.previewHash, |
| | | previewTime: preview.value.previewTime, allowLate: acceptLate.value }); |
| | | message.success(`æäº§æåï¼${ids.value.length} 个工åï¼çæ ${result.taskIds.length} 个任å¡`); |
| | | executing.value = false; |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | } catch (error) { |
| | | errorText.value = error instanceof Error ? error.message : 'æ§è¡ç»ææªç¡®è®¤ï¼è¯·å·æ°åè¡¨å¹¶éæ°é¢è§'; |
| | | // 请æ±å¤±è´¥æç»ææªç¡®è®¤åå¿
é¡»éç®ï¼æ§é¢è§ä¸è½ç´æ¥å次æäº¤ã |
| | | preview.value = undefined; |
| | | acceptLate.value = false; |
| | | emit('success'); // å·æ°å·¥ååçç¹å¾ï¼æ ¸å¯¹è¶
æ¶è¯·æ±æ¯å¦å·²ç»æäº¤ã |
| | | } finally { |
| | | executing.value = false; |
| | | modalApi.unlock(); |
| | | } |
| | | } |
| | | |
| | | const orderColumns = [ |
| | | { title: 'å·¥åç¼ç ', dataIndex: 'workOrderCode', width: 170 }, |
| | | { title: 'ç产æ°é', dataIndex: 'quantity', width: 100 }, |
| | | { title: 'æ°å¢ä»»å¡', dataIndex: 'taskCount', width: 90 }, |
| | | { title: 'éæ±æ¥æ', dataIndex: 'requestDate', width: 120 }, |
| | | { title: 'é¢è®¡å®æ', dataIndex: 'endTime', width: 175 }, |
| | | { title: 'æ ¡éªç»æ', dataIndex: 'error', width: 300 }, |
| | | ]; |
| | | const taskColumns = [ |
| | | { title: 'å·¥å', dataIndex: 'workOrderCode', width: 160 }, |
| | | { title: 'å·¥åº', dataIndex: 'processName', width: 130 }, |
| | | { title: 'å·¥ä½ç«', dataIndex: 'workstationName', width: 150 }, |
| | | { title: '设å¤', dataIndex: 'machineryNames', width: 180 }, |
| | | { title: 'æ¬æ¬¡æ°é', dataIndex: 'quantity', width: 100 }, |
| | | { title: 'å°æ¶äº§è½', dataIndex: 'hourlyCapacity', width: 100 }, |
| | | { title: 'å·¥æ¶(h)', dataIndex: 'duration', width: 85 }, |
| | | { title: 'å¼å·¥æ¶é´', dataIndex: 'startTime', width: 180 }, |
| | | { title: '宿æ¶é´', dataIndex: 'endTime', width: 180 }, |
| | | ]; |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal title="ä¸é®æäº§" class="w-[min(96vw,1200px)]"> |
| | | <div class="space-y-5 px-1"> |
| | | <Alert type="info" show-icon message="å
æ ¡éªä¸é¢è§ï¼å确认æäº§" |
| | | description="æäº¤æä¼å
åå·¥åºé¡ºåºå®æå·¥ä½ç«ä¸è®¾å¤ï¼æ¯å¤©09:00â18:00ï¼æé¤å¨æ«åå·²é
ç½®è忥ï¼é工使¶é´èªå¨é¡ºå»¶ãå·²é¨åæäº§çå·¥ååªè¡¥é½å©ä½æ°éï¼æå¤30个工åã200个æ°ä»»å¡ã" /> |
| | | <div class="flex flex-wrap items-end gap-3"> |
| | | <label class="flex flex-col gap-2"> |
| | | <span class="font-medium">ææ©å¼å·¥æ¶é´</span> |
| | | <DatePicker v-model:value="startTime" show-time :allow-clear="false" :disabled="executing" |
| | | format="YYYY-MM-DD HH:mm:ss" :disabled-date="(date) => date.endOf('day').isBefore(dayjs())" /> |
| | | </label> |
| | | <Button :loading="loading" :disabled="executing" @click="calculate"> |
| | | {{ preview ? 'éæ°è®¡ç®é¢è§' : 'çææäº§é¢è§' }} |
| | | </Button> |
| | | <span class="text-sm text-muted-foreground">已鿩 {{ ids.length }} 个工å</span> |
| | | </div> |
| | | <Alert v-if="errorText" type="error" show-icon :message="errorText" /> |
| | | <template v-if="preview"> |
| | | <Descriptions :column="{ xs: 1, sm: 3 }" size="small" bordered> |
| | | <Descriptions.Item label="工忰">{{ preview.orders.length }}</Descriptions.Item> |
| | | <Descriptions.Item label="æ°å¢ä»»å¡æ°">{{ preview.tasks.length }}</Descriptions.Item> |
| | | <Descriptions.Item label="é¢è§æææ">15åéï¼æäº¤æ¶éæ°æ ¡éª</Descriptions.Item> |
| | | </Descriptions> |
| | | <Alert v-if="!preview.canExecute" type="error" show-icon message="é¨åå·¥åæªéè¿æ ¡éªï¼æ¬æ¹æ¬¡ä¸è½æäº¤" |
| | | description="è¯·æ ¹æ®ä¸æ¹æ ¡éªç»æä¿®æ£å·¥åãè®¾å¤æå·²æä»»å¡çé
ç½®ï¼åéæ°è®¡ç®é¢è§ã" /> |
| | | <div> |
| | | <h3 class="mb-2 font-medium">å·¥åæ ¡éª</h3> |
| | | <Table :columns="orderColumns" :data-source="preview.orders" row-key="workOrderId" |
| | | :pagination="false" size="small" :scroll="{ x: 1055 }"> |
| | | <template #bodyCell="{ column, record, text }"> |
| | | <template v-if="column.dataIndex === 'error'"> |
| | | <span v-if="record.error" class="text-destructive">{{ record.error }}</span> |
| | | <Tag v-else :color="record.late ? 'orange' : 'green'">{{ record.late ? 'æå»¶æé£é©' : 'éè¿' }}</Tag> |
| | | </template> |
| | | <template v-else-if="column.dataIndex === 'requestDate'">{{ text ? String(text).slice(0, 10) : 'æªæå®' }}</template> |
| | | </template> |
| | | </Table> |
| | | </div> |
| | | <div v-if="preview.tasks.length"> |
| | | <h3 class="mb-2 font-medium">ä»»å¡å®æ</h3> |
| | | <Table :columns="taskColumns" :data-source="preview.tasks" |
| | | :row-key="(row) => `${row.workOrderId}-${row.processId}`" :pagination="{ pageSize: 10, showSizeChanger: false }" |
| | | size="small" :scroll="{ x: 1365 }" /> |
| | | </div> |
| | | <Alert v-if="preview.notices.length" type="info" show-icon message="èäº§åºæç¤º"> |
| | | <template #description> |
| | | <p v-for="notice in preview.notices" :key="notice">{{ notice }}</p> |
| | | </template> |
| | | </Alert> |
| | | <Alert v-if="preview.warnings.length" type="warning" show-icon message="è¯·æ ¸å¯¹äº¤æé£é©"> |
| | | <template #description> |
| | | <p v-for="warning in preview.warnings" :key="warning">{{ warning }}</p> |
| | | <Checkbox v-model:checked="acceptLate" :disabled="executing" class="mt-2"> |
| | | æå·²æ ¸å¯¹å¹¶æ¥å以ä¸äº¤æå»¶æé£é© |
| | | </Checkbox> |
| | | </template> |
| | | </Alert> |
| | | </template> |
| | | </div> |
| | | </Modal> |
| | | </template> |
| | |
| | | getWorkOrderProgress, |
| | | checkAndAutoFinishWorkOrder, |
| | | } from '#/api/mes/pro/workorder'; |
| | | import { previewAutoGenerate, autoGenerateTasks } from '#/api/mes/pro/task'; |
| | | |
| | | |
| | | import ArchiveDetail from '#/views/mes/pd/archive/modules/detail.vue'; |
| | | import { useGridColumns, useGridFormSchema, useTaskGridColumns } from './data'; |
| | | import QuickFeedbackForm from './modules/quick-feedback.vue'; |
| | | import SchedulePreviewModal from './modules/schedule-preview.vue'; |
| | | import AutoSchedule from '../task/modules/auto-schedule.vue'; |
| | | |
| | | defineOptions({ name: 'WorkorderCenter' }); |
| | | const archiveDetailRef = ref<InstanceType<typeof ArchiveDetail>>(); |
| | |
| | | }); |
| | | |
| | | const [SchedulePreviewModalWrap, schedulePreviewModalApi] = useVbenModal({ |
| | | connectedComponent: SchedulePreviewModal, |
| | | connectedComponent: AutoSchedule, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | |
| | | .open(); |
| | | } |
| | | |
| | | /** ä¸é®æäº§é¢è§ */ |
| | | async function handleSchedulePreview(row: MesProWorkOrderApi.WorkOrder) { |
| | | try { |
| | | const preview = await previewAutoGenerate({ workOrderId: row.id! }); |
| | | schedulePreviewModalApi |
| | | .setData({ workOrder: row, preview }) |
| | | .open(); |
| | | } catch { |
| | | message.error('è·åæäº§é¢è§å¤±è´¥'); |
| | | } |
| | | } |
| | | |
| | | /** æ§è¡ä¸é®æäº§ */ |
| | | async function handleScheduleExecute(row: MesProWorkOrderApi.WorkOrder) { |
| | | try { |
| | | const result = await autoGenerateTasks({ workOrderId: row.id! }); |
| | | message.success(`æäº§æåï¼çæ ${result.taskIds?.length || 0} 个任å¡`); |
| | | handleRefresh(); |
| | | } catch { |
| | | message.error('æäº§å¤±è´¥'); |
| | | } |
| | | function handleSchedulePreview(row: MesProWorkOrderApi.WorkOrder) { |
| | | schedulePreviewModalApi.setData({ workOrderIds: [row.id!] }).open(); |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'æäº§é¢è§', |
| | | type: 'link', |
| | | auth: ['mes:pro-task:create'], |
| | | ifShow: row.quantityScheduled < row.quantity, |
| | | onClick: handleSchedulePreview.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'ä¸é®æäº§', |
| | | type: 'link', |
| | | auth: ['mes:pro-task:create'], |
| | | ifShow: row.quantityScheduled < row.quantity, |
| | | popConfirm: { |
| | | title: '确认ä¸é®æäº§ï¼å°æç
§å·¥èºè·¯çº¿èªå¨çæä»»å¡ã', |
| | | confirm: handleScheduleExecute.bind(null, row), |
| | | }, |
| | | ifShow: row.status === MesProWorkOrderStatusEnum.CONFIRMED && row.type === MesProWorkOrderTypeEnum.SELF && (row.quantity ?? 0) > 0 && (row.quantityProduced ?? 0) < (row.quantity ?? 0), |
| | | onClick: handleSchedulePreview.bind(null, row), |
| | | }, |
| | | { |
| | | label: '彿¡£', |
| | |
| | | onChange: () => syncUnqualified(formApi), |
| | | }, |
| | | defaultValue: 0, |
| | | rules: 'required', |
| | | dependencies: { |
| | | triggerFields: ['unqualifiedQuantity'], |
| | | show: (values) => |
| | |
| | | onChange: () => syncUnqualified(formApi), |
| | | }, |
| | | defaultValue: 0, |
| | | rules: 'required', |
| | | dependencies: { |
| | | triggerFields: ['unqualifiedQuantity'], |
| | | show: (values) => |
| | |
| | | onChange: () => syncUnqualified(formApi), |
| | | }, |
| | | defaultValue: 0, |
| | | rules: 'required', |
| | | dependencies: { |
| | | triggerFields: ['unqualifiedQuantity'], |
| | | show: (values) => |
| | |
| | | placeholder: 'è¯·éæ©æ£æµäººå', |
| | | valueField: 'id', |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'inspectDate', |
| | |
| | | placeholder: 'è¯·éæ©æ£æµæ¥æ', |
| | | valueFormat: 'YYYY-MM-DD', |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'checkResult', |
| | |
| | | placeholder: 'è¯·éæ©æ¥ææ¥æ', |
| | | valueFormat: 'YYYY-MM-DD', |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'inspectorUserId', |
| | |
| | | placeholder: 'è¯·éæ©æ£æµäººå', |
| | | valueField: 'id', |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'inspectDate', |
| | |
| | | placeholder: 'è¯·éæ©æ£æµæ¥æ', |
| | | valueFormat: 'YYYY-MM-DD', |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'checkResult', |
| | |
| | | componentProps: { |
| | | placeholder: 'è¯·éæ©å®¢æ·', |
| | | }, |
| | | rules: 'selectRequired', |
| | | dependencies: { |
| | | triggerFields: ['sourceDocId'], |
| | | componentProps: (values) => ({ |
| | |
| | | placeholder: 'è¯·éæ©æ£æµäººå', |
| | | valueField: 'id', |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'outDate', |
| | |
| | | placeholder: 'è¯·éæ©åºè´§æ¥æ', |
| | | valueFormat: 'YYYY-MM-DD', |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'inspectDate', |
| | |
| | | placeholder: 'è¯·éæ©æ£æµæ¥æ', |
| | | valueFormat: 'YYYY-MM-DD', |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'checkResult', |
| | |
| | | width: 80, |
| | | }, |
| | | { |
| | | field: 'canInspect', |
| | | title: '坿£éª', |
| | | width: 100, |
| | | align: 'center', |
| | | slots: { default: 'canInspect' }, |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | | width: 130, |
| | | fixed: 'right', |
| | |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | import { MesQcTypeEnum } from '@vben/constants'; |
| | | import { Alert, Tag } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { getPendingInspectPage } from '#/api/mes/qc/pendinginspect'; |
| | |
| | | <OqcFormModal @success="handleRefresh" /> |
| | | <RqcFormModal @success="handleRefresh" /> |
| | | |
| | | <Alert |
| | | class="mb-4" |
| | | type="info" |
| | | show-icon |
| | | message="ä»å¾
æ£ä»»å¡å建æ£éªå" |
| | | description="请å
ä¸ºç©æé
置并å¯ç¨å¯¹åºç±»åçè´¨æ£æ¹æ¡ãæªæ¾ç¤ºã坿£éªãçç©ææ æ³å建æ£éªåãç产è¿ç¨æ£éªå®æåï¼ç³»ç»ä¼å忥工并å¤çåæ ¼äº§åºã" |
| | | /> |
| | | <Grid table-title="å¾
æ£ä»»å¡å表"> |
| | | <template #canInspect="{ row }"> |
| | | <Tag v-if="row.canInspect" color="success">坿£éª</Tag> |
| | | <Tag v-else color="error">æªé
置模æ¿</Tag> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | |
| | | type: 'link', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['mes:qc-iqc:create'], |
| | | disabled: row.canInspect === false, |
| | | ifShow: row.qcType === MesQcTypeEnum.IQC, |
| | | onClick: handleCreateIqc.bind(null, row), |
| | | }, |
| | |
| | | type: 'link', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['mes:qc-ipqc:create'], |
| | | disabled: row.canInspect === false, |
| | | ifShow: row.qcType === MesQcTypeEnum.IPQC, |
| | | onClick: handleCreateIpqc.bind(null, row), |
| | | }, |
| | |
| | | type: 'link', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['mes:qc-oqc:create'], |
| | | disabled: row.canInspect === false, |
| | | ifShow: row.qcType === MesQcTypeEnum.OQC, |
| | | onClick: handleCreateOqc.bind(null, row), |
| | | }, |
| | |
| | | type: 'link', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['mes:qc-rqc:create'], |
| | | disabled: row.canInspect === false, |
| | | ifShow: row.qcType === MesQcTypeEnum.RQC, |
| | | onClick: handleCreateRqc.bind(null, row), |
| | | }, |
| | |
| | | showTime: true, |
| | | valueFormat: 'YYYY-MM-DD HH:mm:ss', |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'inspectorUserId', |
| | |
| | | placeholder: 'è¯·éæ©æ£æµäººå', |
| | | valueField: 'id', |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'checkResult', |
| | |
| | | try { |
| | | const [result, counts] = await Promise.all([ |
| | | getWorkstationPage({ pageNo: 1, pageSize: 200 }), |
| | | getPendingFeedbackCountByWorkstation().catch(() => null), |
| | | getPendingFeedbackCountByWorkstation(true).catch(() => null), |
| | | ]); |
| | | workstations.value = result.list ?? []; |
| | | pendingCounts.value = counts; |
| | |
| | | return pendingCounts.value?.[String(ws.id)] ?? 0; |
| | | } |
| | | |
| | | /** è¿æ»¤åçå·¥ä½ç« */ |
| | | /** è¿æ»¤åçå·¥ä½ç«ï¼ä»
å±ç¤ºå½åç¨æ·æå¯æ¥å·¥ä»»å¡çï¼ç»è®¡å¤±è´¥æ¶ä¸è¿æ»¤ï¼é¿å
å
¨é¨éè */ |
| | | const filtered = computed(() => { |
| | | const kw = searchKeyword.value.trim().toLowerCase(); |
| | | if (!kw) return workstations.value; |
| | | return workstations.value.filter((ws) => { |
| | | if (pendingCounts.value && getPendingCount(ws) <= 0) return false; |
| | | if (!kw) return true; |
| | | const fields = [ws.code, ws.name, ws.processName, ws.workshopName, ws.address]; |
| | | return fields.some((f) => String(f ?? '').toLowerCase().includes(kw)); |
| | | }); |
| | | }); |
| | | |
| | | /** ç©ºç¶æææ¡ */ |
| | | const emptyText = computed(() => { |
| | | if (!pendingCounts.value) return 'ææ å·¥ä½ç«æ°æ®'; |
| | | if (searchKeyword.value.trim()) return '没æå¹é
çå·¥ä½ç«'; |
| | | return 'ææ å¯æ¥å·¥çå·¥ä½ç«'; |
| | | }); |
| | | |
| | | /** 鿩工ä½ç« */ |
| | |
| | | <!-- ç©ºç¶æ --> |
| | | <div v-if="!filtered.length && !loading" class="ws-select__empty"> |
| | | <IconifyIcon icon="lucide:inbox" class="ws-select__empty-icon" /> |
| | | <p>ææ å·¥ä½ç«æ°æ®</p> |
| | | <p>{{ emptyText }}</p> |
| | | </div> |
| | | |
| | | <!-- å¡çç½æ ¼ --> |
| | |
| | | return data; |
| | | } |
| | | |
| | | /** å è½½ checkFlag */ |
| | | async function resolveCheckFlag(routeId?: number, processId?: number) { |
| | | /** å è½½ checkFlagï¼ä»»å¡ï¼å·¥åå·¥åºå¿«ç
§ï¼ä¼å
ï¼ç¼ºå¤±æ¶åéå·¥èºè·¯çº¿å·¥åº */ |
| | | async function resolveCheckFlag( |
| | | routeId?: number, |
| | | processId?: number, |
| | | taskCheckFlag?: boolean, |
| | | ) { |
| | | if (typeof taskCheckFlag === 'boolean') return taskCheckFlag; |
| | | if (!routeId || !processId) return true; |
| | | try { |
| | | const rp = await getRouteProcessByRouteAndProcess(routeId, processId); |
| | |
| | | formApi.setState({ schema: useFormSchema(formType.value, formApi) }); |
| | | |
| | | const code = await generateAutoCode(MesAutoCodeRuleCode.PRO_FEEDBACK_CODE); |
| | | const checkFlag = await resolveCheckFlag(task.routeId, task.processId); |
| | | const checkFlag = await resolveCheckFlag( |
| | | task.routeId, |
| | | task.processId, |
| | | task.checkFlag, |
| | | ); |
| | | |
| | | await formApi.setValues({ |
| | | code, |
| | |
| | | feedbackStatus.value = data.status; |
| | | formType.value = 'update'; |
| | | formApi.setState({ schema: useFormSchema(formType.value, formApi) }); |
| | | const checkFlag = await resolveCheckFlag(data.routeId, data.processId); |
| | | const checkFlag = await resolveCheckFlag( |
| | | data.routeId, |
| | | data.processId, |
| | | data.checkFlag, |
| | | ); |
| | | await formApi.setValues({ ...data, checkFlag }); |
| | | originalSnapshot.value = JSON.stringify(await getAlignedData(false)); |
| | | } |
| | |
| | | workstationId: workstationId.value, |
| | | pageNo: 1, |
| | | pageSize: 200, |
| | | onlyMine: true, |
| | | }); |
| | | taskList.value = result.list ?? []; |
| | | // 忥 selectedTask å°ææ°æ°æ®ï¼ä¸èªå¨éä¸ç¬¬ä¸ä¸ªï¼éç¨æ·æå¨ç¹å»ï¼ |
| | |
| | | |
| | | // --- è¿åç³»ç» --- |
| | | function goBack() { |
| | | router.push('/'); |
| | | // è¿åä¸ä¸æ¥ï¼ç´æ¥æå¼å·¥ä½å°ï¼æ åå²è®°å½ï¼æ¶å
åºåé¦é¡µ |
| | | if (window.history.state?.back) { |
| | | router.back(); |
| | | } else { |
| | | router.push('/'); |
| | | } |
| | | } |
| | | </script> |
| | | |
| | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { MesWmArrivalNoticeStatusEnum } from '@vben/constants'; |
| | | |
| | | import { Button, Divider, message, Popconfirm } from 'ant-design-vue'; |
| | | import { Alert, Button, Divider, message, Popconfirm } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { |
| | |
| | | originalSnapshot.value = current; |
| | | } |
| | | await submitArrivalNotice(formData.value.id); |
| | | message.success('æäº¤æå'); |
| | | message.success('æäº¤æåï¼è¯·æåæ®ç¶æç»§ç»è´¨æ£æå
¥åº'); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | } finally { |
| | |
| | | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-3/5"> |
| | | <Alert v-if="isEditable" class="mx-4 mb-4" type="info" show-icon message="æäº¤å请å®åç©ææç»" description="éæ£ç©æè¯·å
é
ç½®å¯ç¨çæ¥ææ£éªæ¨¡æ¿ãæäº¤åç³»ç»ä¼çææ£éªåï¼æ£éªå®æåååçå
¥åºãä¸éæ£çç©æå¯ç´æ¥è¿å
¥å¾
å
¥åºã" /> |
| | | <Form class="mx-4" /> |
| | | <!-- éæ°å»ºæ¨¡å¼å±ç¤ºç©æä¿¡æ¯ --> |
| | | <template v-if="formData?.id"> |
| | |
| | | formatter: 'formatDate', |
| | | }, |
| | | { |
| | | field: 'produceId', |
| | | title: 'æ¥æº', |
| | | width: 110, |
| | | align: 'center', |
| | | slots: { default: 'source' }, |
| | | }, |
| | | { |
| | | field: 'status', |
| | | title: 'åæ®ç¶æ', |
| | | minWidth: 110, |
| | |
| | | import { MesWmProductReceiptStatusEnum } from '@vben/constants'; |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | |
| | | import { Button, message } from 'ant-design-vue'; |
| | | import { Button, message, Tag } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | |
| | | {{ row.code }} |
| | | </Button> |
| | | </template> |
| | | <template #source="{ row }"> |
| | | <Tag v-if="row.produceId" color="blue">产åºèªå¨çæ</Tag> |
| | | <Tag v-else>æå·¥å建</Tag> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | |
| | | import { confirm, useVbenModal } from '@vben/common-ui'; |
| | | import { MesWmProductReceiptStatusEnum } from '@vben/constants'; |
| | | |
| | | import { Button, Divider, message, Popconfirm } from 'ant-design-vue'; |
| | | import { Alert, Button, Divider, message, Popconfirm } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { |
| | |
| | | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-3/5"> |
| | | <Alert |
| | | v-if="formData?.produceId" |
| | | class="mx-4 mb-3" |
| | | type="info" |
| | | show-icon |
| | | message="产åºèªå¨çæçå
¥åºå" |
| | | description="è¯¥åæ®ç±ç产产åºèªå¨çæï¼åºåå·²å¨äº§åºæ¶å
¥è´¦ãæ§è¡å
¥åºä»
å
³éä¸å¡åæ®ï¼ä¸ä¼é夿´æ°åºåã" |
| | | /> |
| | | <Form class="mx-4" /> |
| | | <!-- éæ°å»ºæ¨¡å¼å±ç¤ºç©æä¿¡æ¯ --> |
| | | <template v-if="formData?.id"> |