fix: 新疆-绿能
1.报工等待时长倒计时 + 报工数量可选
2.报工自动生成产品入库单 + 勾选入库 + 不合格仓库兜底
| | |
| | | status?: number; |
| | | remark?: string; |
| | | createTime?: Date; |
| | | waitDuration?: number; // çå¾
æ¶é¿ï¼å°æ¶ï¼ï¼æ¥å·¥åéå计æ¶çå¾
|
| | | needReportQuantity?: boolean; // æ¯å¦éè¦å¡«åæ¥å·¥æ°é |
| | | } |
| | | } |
| | | |
| | |
| | | remark?: string; |
| | | bomItems?: ProcessBomItem[]; |
| | | createTime?: Date; |
| | | waitDuration?: number; // çå¾
æ¶é¿ï¼å°æ¶ï¼ï¼>0 表示æ¥å·¥åéå计æ¶çå¾
|
| | | needReportQuantity?: boolean; // æ¯å¦éè¦å¡«åæ¥å·¥æ°éï¼false 表示æ¥å·¥æ°éå
许为空 |
| | | } |
| | | } |
| | | |
| | |
| | | reportableQuantity?: number; // 坿¥å·¥æ°é |
| | | assigneeUserIds?: number[]; // æ¥å·¥äºº |
| | | feedbackApprove?: boolean; // æ¯å¦æ¥å·¥å®¡æ¹ï¼æ´¾çèªå·¥åï¼ |
| | | waitStartTime?: string; // æ¥å·¥å计æ¶å¼å§æ¶é´ï¼yyyy-MM-dd HH:mm:ssï¼ |
| | | } |
| | | |
| | | /** MES ç产任å¡å页æ¥è¯¢åæ° */ |
| | |
| | | export function resumeTask(data: { id: number }) { |
| | | return requestClient.put('/mes/pro/task/resume', data); |
| | | } |
| | | |
| | | /** å¼å§æ¥å·¥å计æ¶ï¼è¿åå计æ¶å¼å§æ¶é´ï¼yyyy-MM-dd HH:mm:ssï¼ */ |
| | | export function startWaitCountdown(taskId: number) { |
| | | return requestClient.post<string>('/mes/pro/task/start-wait-countdown', { |
| | | taskId, |
| | | }); |
| | | } |
| | |
| | | receiptDate?: number; // å
¥åºæ¥æ |
| | | status?: number; // åæ®ç¶æ |
| | | remark?: string; // 夿³¨ |
| | | feedbackId?: number; // æ¥æºæ¥å·¥ç¼å·ï¼éç©ºè¡¨ç¤ºç±æ¥å·¥èªå¨çæï¼ |
| | | createTime?: Date; // å建æ¶é´ |
| | | } |
| | | |
| | | /** 䏿¶åé
æç» */ |
| | | export interface StockDetail { |
| | | warehouseId: number; // ä»åºç¼å· |
| | | locationId: number; // åºåºç¼å· |
| | | areaId: number; // åºä½ç¼å· |
| | | quantity: number; // åé
æ°é |
| | | } |
| | | |
| | | /** 䏿¶è¡ */ |
| | | export interface StockRow { |
| | | lineId: number; // å
¥åºåè¡ç¼å· |
| | | isStock: boolean; // æ¯å¦å
¥åº |
| | | details?: StockDetail[]; // isStock=true æ¶çä»åº/åºåº/åºä½åé
|
| | | } |
| | | |
| | | /** æ¹é䏿¶è¯·æ± */ |
| | | export interface StockRequest { |
| | | receiptId: number; // 产åå
¥åºåç¼å· |
| | | rows: StockRow[]; // è¡ä¸æ¶å表 |
| | | } |
| | | } |
| | | |
| | |
| | | ); |
| | | } |
| | | |
| | | /** æ¹é䏿¶ï¼å¾éæ¯å¦å
¥åº + åé
ä»åº/åºåº/åºä½æ°éï¼ */ |
| | | export function submitProductReceiptStock( |
| | | data: MesWmProductReceiptApi.StockRequest, |
| | | ) { |
| | | return requestClient.post<boolean>( |
| | | '/mes/wm/product-receipt/submit-stock', |
| | | data, |
| | | ); |
| | | } |
| | | |
| | | /** 导åºäº§åå
¥åºå */ |
| | | export function exportProductReceipt(params: any) { |
| | | return requestClient.download('/mes/wm/product-receipt/export-excel', { |
| | |
| | | quantity?: number; // å
¥åºæ°é |
| | | batchId?: number; // æ¹æ¬¡ç¼å· |
| | | batchCode?: string; // æ¹æ¬¡å· |
| | | qualityStatus?: number; // è´¨éç¶æï¼åå
¸ MES_WM_QUALITY_STATUS åæ ¼/ä¸åæ ¼ï¼ |
| | | isStock?: boolean; // æ¯å¦å
¥åºï¼false/空=æ§è¡å
¥åºæ¶é»è®¤è¿ä¸åæ ¼ä»åºï¼ |
| | | remark?: string; // 夿³¨ |
| | | } |
| | | } |
| | |
| | | import { generateAutoCode } from '#/api/mes/md/autocode/record'; |
| | | import { getRouteProcessByRouteAndProcess } from '#/api/mes/pro/route/process'; |
| | | import { getSimpleUserList } from '#/api/system/user'; |
| | | import { loadParamDetails } from './param-state'; |
| | | import { |
| | | loadParamDetails, |
| | | resetWaitConfig, |
| | | waitDuration, |
| | | waitStartTime, |
| | | waitTaskId, |
| | | } from './param-state'; |
| | | import { getRangePickerDefaultProps } from '#/utils'; |
| | | import { MdItemSelect } from '#/views/mes/md/item/components'; |
| | | import { MdWorkstationSelect } from '#/views/mes/md/workstation/components'; |
| | |
| | | defaultValue: true, |
| | | }, |
| | | { |
| | | fieldName: 'needReportQuantity', |
| | | component: 'Input', |
| | | dependencies: { triggerFields: [''], show: () => false }, |
| | | defaultValue: true, |
| | | }, |
| | | { |
| | | fieldName: 'routeId', |
| | | component: 'Input', |
| | | dependencies: { triggerFields: [''], show: () => false }, |
| | |
| | | status: MesProWorkOrderStatusEnum.CONFIRMED, |
| | | // 订ååæ´ï¼æ¸
空任å¡åä»»å¡å¸¦åºç产åä¿¡æ¯ãæ°éåºåæ§å¶ä½ |
| | | onChange: async () => { |
| | | resetWaitConfig(); |
| | | await formApi?.setValues({ |
| | | checkFlag: true, |
| | | needReportQuantity: true, |
| | | itemCode: undefined, |
| | | itemId: undefined, |
| | | itemName: undefined, |
| | |
| | | onChange: async (task?: MesProTaskApi.Task) => { |
| | | await loadParamDetails(task?.id); |
| | | if (!task) { |
| | | resetWaitConfig(); |
| | | await formApi?.setFieldValue('needReportQuantity', true); |
| | | return; |
| | | } |
| | | await formApi?.setValues({ |
| | |
| | | workstationId: task.workstationId, |
| | | reportableQuantity: task.reportableQuantity, |
| | | }); |
| | | // å·¥èºè·¯çº¿å·¥åºç checkFlag å³å®æ°éåºåå±ç¤º |
| | | waitTaskId.value = task.id; |
| | | waitStartTime.value = task.waitStartTime; |
| | | // å·¥èºè·¯çº¿å·¥åºç checkFlag / çå¾
æ¶é¿ / æ¥å·¥æ°éè§åå³å®å±ç¤º |
| | | if (task.routeId && task.processId) { |
| | | try { |
| | | const routeProcess = await getRouteProcessByRouteAndProcess( |
| | |
| | | 'checkFlag', |
| | | routeProcess?.checkFlag ?? false, |
| | | ); |
| | | await formApi?.setFieldValue( |
| | | 'needReportQuantity', |
| | | routeProcess?.needReportQuantity ?? true, |
| | | ); |
| | | waitDuration.value = routeProcess?.waitDuration; |
| | | } catch { |
| | | await formApi?.setFieldValue('checkFlag', true); |
| | | await formApi?.setFieldValue('needReportQuantity', true); |
| | | waitDuration.value = undefined; |
| | | } |
| | | } else { |
| | | waitDuration.value = undefined; |
| | | await formApi?.setFieldValue('needReportQuantity', true); |
| | | } |
| | | }, |
| | | }, |
| | |
| | | precision: 2, |
| | | placeholder: '请è¾å
¥æ¥å·¥æ°é', |
| | | }, |
| | | rules: 'required', |
| | | dependencies: { |
| | | triggerFields: ['needReportQuantity'], |
| | | show: (values) => values.needReportQuantity !== false, |
| | | rules: (values) => |
| | | values.needReportQuantity === false ? null : 'required', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'feedbackUserId', |
| | |
| | | updateFeedback, |
| | | } from '#/api/mes/pro/feedback'; |
| | | import { getRouteProcessByRouteAndProcess } from '#/api/mes/pro/route/process'; |
| | | import { getTask } from '#/api/mes/pro/task'; |
| | | import { getWorkOrder } from '#/api/mes/pro/workorder'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useFormSchema } from '../data'; |
| | | import { findMissingRequiredParams, loadParamDetails, paramDetails } from '../param-state'; |
| | | import { |
| | | findMissingRequiredParams, |
| | | loadParamDetails, |
| | | paramDetails, |
| | | resetWaitConfig, |
| | | waitDuration, |
| | | waitStartTime, |
| | | waitTaskId, |
| | | } from '../param-state'; |
| | | import BatchInputDialog from './batch-input-dialog.vue'; |
| | | import ItemConsumeList from './item-consume-list.vue'; |
| | | import ProductProduceList from './product-produce-list.vue'; |
| | | import WaitCountdown from './wait-countdown.vue'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | const formType = ref<FormType>('create'); |
| | |
| | | }); |
| | | |
| | | /** æäº¤å坹齿°éï¼æ¥å·¥åªå¡«åæ¥å·¥æ°éï¼åæ ¼/ä¸è¯/åºåæ°éç±è´¨æ£ç¡®å®ï¼æ¥å·¥ç¯èä¸éé */ |
| | | function alignQuantity(data: MesProFeedbackApi.Feedback) { |
| | | function alignQuantity( |
| | | data: MesProFeedbackApi.Feedback, |
| | | needReportQuantity: boolean, |
| | | ) { |
| | | if (!needReportQuantity) { |
| | | data.feedbackQuantity = 0; |
| | | } |
| | | data.uncheckQuantity = data.checkFlag ? data.feedbackQuantity : 0; |
| | | delete data.qualifiedQuantity; |
| | | delete data.unqualifiedQuantity; |
| | |
| | | |
| | | /** 读åå¹¶ææäº¤è§å对é½åçè¡¨åæ°æ®ï¼ç¨äºå¿«ç
§åä¿å */ |
| | | async function getAlignedData(): Promise<MesProFeedbackApi.Feedback> { |
| | | const data = (await formApi.getValues()) as MesProFeedbackApi.Feedback; |
| | | const data = (await formApi.getValues()) as MesProFeedbackApi.Feedback & { |
| | | needReportQuantity?: boolean; |
| | | }; |
| | | syncParamValues(data); |
| | | alignQuantity(data); |
| | | alignQuantity(data, data.needReportQuantity !== false); |
| | | delete data.needReportQuantity; |
| | | return data; |
| | | } |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | /** å 载工åºç checkFlag ç¨äºåæ¾æ°éåºå */ |
| | | async function resolveCheckFlag(routeId?: number, processId?: number) { |
| | | /** å 载工åºé
ç½®ï¼checkFlag / çå¾
æ¶é¿ / æ¥å·¥æ°éè§åï¼ç¨äºåæ¾ */ |
| | | async function resolveRouteProcessConfig(routeId?: number, processId?: number) { |
| | | if (!routeId || !processId) { |
| | | return true; |
| | | return { checkFlag: true, needReportQuantity: true }; |
| | | } |
| | | try { |
| | | const routeProcess = await getRouteProcessByRouteAndProcess( |
| | | routeId, |
| | | processId, |
| | | ); |
| | | return routeProcess?.checkFlag ?? false; |
| | | waitDuration.value = routeProcess?.waitDuration; |
| | | return { |
| | | checkFlag: routeProcess?.checkFlag ?? false, |
| | | needReportQuantity: routeProcess?.needReportQuantity ?? true, |
| | | }; |
| | | } catch { |
| | | return true; |
| | | waitDuration.value = undefined; |
| | | return { checkFlag: true, needReportQuantity: true }; |
| | | } |
| | | } |
| | | |
| | |
| | | subTabsName.value = 'itemConsume'; |
| | | originalSnapshot.value = ''; |
| | | paramDetails.value = []; |
| | | resetWaitConfig(); |
| | | return; |
| | | } |
| | | // å è½½æ°æ® |
| | |
| | | try { |
| | | formData.value = await getFeedback(data.id); |
| | | await loadParamDetails(formData.value.taskId); |
| | | const checkFlag = await resolveCheckFlag( |
| | | const config = await resolveRouteProcessConfig( |
| | | formData.value.routeId, |
| | | formData.value.processId, |
| | | ); |
| | | waitTaskId.value = formData.value.taskId; |
| | | waitStartTime.value = undefined; |
| | | if (formData.value.taskId) { |
| | | try { |
| | | const task = await getTask(formData.value.taskId); |
| | | waitStartTime.value = task?.waitStartTime; |
| | | } catch { |
| | | // ä»»å¡ä¸å卿¶å¿½ç¥å计æ¶åæ¾ |
| | | } |
| | | } |
| | | // è®¾ç½®å° values |
| | | await formApi.setValues({ ...formData.value, checkFlag }); |
| | | await formApi.setValues({ |
| | | ...formData.value, |
| | | checkFlag: config.checkFlag, |
| | | needReportQuantity: config.needReportQuantity, |
| | | }); |
| | | // è®°å½åå§å¿«ç
§ï¼å«æ°é对é½ç»æï¼ï¼submit æ¶ç¨äºè·³è¿æ åæ´çä¿åè¯·æ± |
| | | originalSnapshot.value = JSON.stringify(await getAlignedData()); |
| | | } finally { |
| | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-3/5"> |
| | | <Form class="mx-4" /> |
| | | <div |
| | | v-if="waitDuration" |
| | | class="mx-4 mt-4 rounded border border-gray-200 p-4" |
| | | > |
| | | <div class="mb-2 font-medium">æ¥å·¥å计æ¶</div> |
| | | <WaitCountdown |
| | | :task-id="waitTaskId" |
| | | :wait-duration="waitDuration" |
| | | :wait-start-time="waitStartTime" |
| | | :disabled="!isEditable" |
| | | /> |
| | | </div> |
| | | <div v-if="paramDetails.length" class="mx-4 mt-4 rounded border border-gray-200 p-4"> |
| | | <div class="mb-3 font-medium">å·¥èºåæ°</div> |
| | | <div class="grid grid-cols-2 gap-3"> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'; |
| | | |
| | | import dayjs from 'dayjs'; |
| | | |
| | | import { Button, message } from 'ant-design-vue'; |
| | | |
| | | import { startWaitCountdown } from '#/api/mes/pro/task'; |
| | | |
| | | defineOptions({ name: 'WaitCountdown' }); |
| | | |
| | | interface Props { |
| | | taskId?: number; |
| | | /** çå¾
æ¶é¿ï¼å°æ¶ï¼ï¼>0 æå±ç¤ºåè®¡æ¶ */ |
| | | waitDuration?: number; |
| | | /** å计æ¶å¼å§æ¶é´ï¼yyyy-MM-dd HH:mm:ssï¼ */ |
| | | waitStartTime?: string; |
| | | /** åªè¯»æ¨¡å¼ï¼è¯¦æ
/审æ¹ï¼ä¸ä¸å±ç¤ºå¼å§æé® */ |
| | | disabled?: boolean; |
| | | } |
| | | |
| | | const props = withDefaults(defineProps<Props>(), { |
| | | taskId: undefined, |
| | | waitDuration: undefined, |
| | | waitStartTime: undefined, |
| | | disabled: false, |
| | | }); |
| | | |
| | | const starting = ref(false); |
| | | const startTime = ref<string | undefined>(props.waitStartTime); |
| | | const now = ref(Date.now()); |
| | | let timer: ReturnType<typeof setInterval> | undefined; |
| | | |
| | | const showCountdown = computed( |
| | | () => !!props.waitDuration && props.waitDuration > 0, |
| | | ); |
| | | |
| | | const endTime = computed(() => { |
| | | if (!startTime.value || !showCountdown.value) { |
| | | return undefined; |
| | | } |
| | | return dayjs(startTime.value, 'YYYY-MM-DD HH:mm:ss').valueOf() + |
| | | props.waitDuration! * 3600 * 1000; |
| | | }); |
| | | |
| | | const remainingMs = computed(() => { |
| | | if (!endTime.value) { |
| | | return 0; |
| | | } |
| | | return Math.max(0, endTime.value - now.value); |
| | | }); |
| | | |
| | | const counting = computed(() => remainingMs.value > 0); |
| | | |
| | | const remainingText = computed(() => { |
| | | const totalSeconds = Math.ceil(remainingMs.value / 1000); |
| | | const hours = Math.floor(totalSeconds / 3600); |
| | | const minutes = Math.floor((totalSeconds % 3600) / 60); |
| | | const seconds = totalSeconds % 60; |
| | | const pad = (n: number) => String(n).padStart(2, '0'); |
| | | if (hours > 0) { |
| | | return `å©ä½ ${hours} å°æ¶ ${pad(minutes)} å ${pad(seconds)} ç§`; |
| | | } |
| | | return `å©ä½ ${pad(minutes)} å ${pad(seconds)} ç§`; |
| | | }); |
| | | |
| | | function startTimer() { |
| | | if (timer) { |
| | | clearInterval(timer); |
| | | } |
| | | timer = setInterval(() => { |
| | | now.value = Date.now(); |
| | | if (remainingMs.value <= 0) { |
| | | clearInterval(timer); |
| | | timer = undefined; |
| | | } |
| | | }, 1000); |
| | | } |
| | | |
| | | watch( |
| | | () => props.waitStartTime, |
| | | (value) => { |
| | | startTime.value = value; |
| | | if (value) { |
| | | now.value = Date.now(); |
| | | startTimer(); |
| | | } |
| | | }, |
| | | ); |
| | | |
| | | onMounted(() => { |
| | | if (startTime.value) { |
| | | startTimer(); |
| | | } |
| | | }); |
| | | |
| | | onBeforeUnmount(() => { |
| | | if (timer) { |
| | | clearInterval(timer); |
| | | timer = undefined; |
| | | } |
| | | }); |
| | | |
| | | async function handleStart() { |
| | | if (!props.taskId) { |
| | | message.warning('请å
鿩任å¡'); |
| | | return; |
| | | } |
| | | starting.value = true; |
| | | try { |
| | | const time = await startWaitCountdown(props.taskId); |
| | | startTime.value = time; |
| | | now.value = Date.now(); |
| | | startTimer(); |
| | | message.success('å计æ¶å·²å¼å§'); |
| | | } finally { |
| | | starting.value = false; |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <div v-if="showCountdown" class="flex items-center gap-2"> |
| | | <Button |
| | | v-if="!startTime && !disabled" |
| | | type="primary" |
| | | :loading="starting" |
| | | @click="handleStart" |
| | | > |
| | | å¼å§åè®¡æ¶ |
| | | </Button> |
| | | <span v-if="startTime" class="text-sm"> |
| | | <span v-if="counting" class="text-orange-500">{{ remainingText }}</span> |
| | | <span v-else class="text-green-600">å计æ¶å·²ç»æï¼å¯æ¥å·¥</span> |
| | | </span> |
| | | </div> |
| | | </template> |
| | |
| | | .filter((detail) => detail.required && !detail.paramValue?.trim()) |
| | | .map((detail) => detail.paramName || detail.paramCode || ''); |
| | | } |
| | | |
| | | /** å½åæ¥å·¥ä»»å¡ç¼å·ï¼ç¨äºåè®¡æ¶æ¥å£ï¼ */ |
| | | export const waitTaskId = ref<number>(); |
| | | /** å½åä»»å¡å·¥åºççå¾
æ¶é¿ï¼å°æ¶ï¼ï¼>0 表示æ¥å·¥åéåè®¡æ¶ */ |
| | | export const waitDuration = ref<number>(); |
| | | /** å½å任塿¥å·¥å计æ¶å¼å§æ¶é´ï¼yyyy-MM-dd HH:mm:ssï¼ */ |
| | | export const waitStartTime = ref<string>(); |
| | | |
| | | /** éç½®æ¥å·¥å计æ¶ç¸å
³ç¶æ */ |
| | | export function resetWaitConfig() { |
| | | waitTaskId.value = undefined; |
| | | waitDuration.value = undefined; |
| | | waitStartTime.value = undefined; |
| | | } |
| | |
| | | rules: z.number().default(CommonStatusEnum.ENABLE), |
| | | }, |
| | | { |
| | | fieldName: 'waitDuration', |
| | | label: 'çå¾
æ¶é¿(å°æ¶)', |
| | | component: 'InputNumber', |
| | | formItemClass: 'col-span-2', |
| | | componentProps: { |
| | | class: '!w-full', |
| | | min: 0, |
| | | precision: 2, |
| | | placeholder: 'ç空æ 0 表示æ éå计æ¶', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'needReportQuantity', |
| | | label: 'é填忥工æ°é', |
| | | component: 'Switch', |
| | | formItemClass: 'col-span-1', |
| | | defaultValue: true, |
| | | componentProps: { |
| | | checkedChildren: 'æ¯', |
| | | unCheckedChildren: 'å¦', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'attention', |
| | | label: 'å·¥åºè¯´æ', |
| | | component: 'Textarea', |
| | |
| | | modalApi.lock(); |
| | | try { |
| | | formData.value = await getProcess(data.id); |
| | | // è®¾ç½®å° values |
| | | await formApi.setValues(formData.value); |
| | | // è®¾ç½®å° valuesï¼æ§æ°æ®æªé
ç½®æ¥å·¥æ°éå¼å
³æ¶æé»è®¤éè¦å¡«åï¼ |
| | | await formApi.setValues({ |
| | | ...formData.value, |
| | | needReportQuantity: formData.value.needReportQuantity ?? true, |
| | | }); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { quickCreateFeedback } from '#/api/mes/pro/feedback'; |
| | | import { getRouteProcessByRouteAndProcess } from '#/api/mes/pro/route/process'; |
| | | import { getTask } from '#/api/mes/pro/task'; |
| | | import { |
| | | loadParamDetails, |
| | | paramDetails, |
| | | } from '#/views/mes/pro/feedback/param-state'; |
| | | import WaitCountdown from '#/views/mes/pro/feedback/modules/wait-countdown.vue'; |
| | | import ProTaskSelect from '#/views/mes/pro/task/components/select.vue'; |
| | | |
| | | defineOptions({ name: 'QuickFeedbackForm' }); |
| | |
| | | feedbackQuantity: undefined, |
| | | remark: '', |
| | | }; |
| | | // é¢è®¾ä»»å¡æ¶å è½½å
¶å·¥èºåæ°ï¼å¦åçå¾
鿩任å¡ååå è½½ |
| | | waitDuration.value = undefined; |
| | | needReportQuantity.value = true; |
| | | waitStartTime.value = undefined; |
| | | // é¢è®¾ä»»å¡æ¶å è½½å
¶å·¥èºåæ°ä¸çå¾
é
ç½®ï¼å¦åçå¾
鿩任å¡ååå è½½ |
| | | if (props.taskId) { |
| | | loadParamDetails(props.taskId); |
| | | getTask(props.taskId) |
| | | .then(applyTaskWaitConfig) |
| | | .catch(() => {}); |
| | | } else { |
| | | paramDetails.value = []; |
| | | } |
| | |
| | | remark: '', |
| | | }); |
| | | const submitting = ref(false); |
| | | const waitDuration = ref<number>(); |
| | | const needReportQuantity = ref<boolean>(true); |
| | | const waitStartTime = ref<string>(); |
| | | |
| | | const hasPredefinedTask = computed(() => !!props.taskId); |
| | | |
| | |
| | | selectedTask.value = task; |
| | | selectedTaskId.value = task?.id; |
| | | loadParamDetails(task?.id); |
| | | applyTaskWaitConfig(task); |
| | | } |
| | | |
| | | /** æä»»å¡å·¥åºå è½½çå¾
æ¶é¿ä¸æ¥å·¥æ°éè§å */ |
| | | async function applyTaskWaitConfig(task?: MesProTaskApi.Task) { |
| | | waitDuration.value = undefined; |
| | | needReportQuantity.value = true; |
| | | waitStartTime.value = task?.waitStartTime; |
| | | if (!task?.routeId || !task?.processId) { |
| | | return; |
| | | } |
| | | try { |
| | | const routeProcess = await getRouteProcessByRouteAndProcess( |
| | | task.routeId, |
| | | task.processId, |
| | | ); |
| | | waitDuration.value = routeProcess?.waitDuration; |
| | | needReportQuantity.value = routeProcess?.needReportQuantity ?? true; |
| | | } catch { |
| | | // å 载失败æé»è®¤å¤çï¼æ éå计æ¶ãæ°éå¿
å¡«ï¼ |
| | | } |
| | | } |
| | | |
| | | async function handleSubmit() { |
| | |
| | | message.warning('è¯·éæ©ä»»å¡'); |
| | | return; |
| | | } |
| | | if (!formData.value.feedbackQuantity) { |
| | | if (needReportQuantity.value && !formData.value.feedbackQuantity) { |
| | | message.warning('请è¾å
¥æ¥å·¥æ°é'); |
| | | return; |
| | | } |
| | |
| | | try { |
| | | await quickCreateFeedback({ |
| | | taskId, |
| | | feedbackQuantity: formData.value.feedbackQuantity, |
| | | feedbackQuantity: needReportQuantity.value |
| | | ? (formData.value.feedbackQuantity ?? 0) |
| | | : 0, |
| | | paramValues: paramDetails.value.map((detail) => ({ |
| | | detailId: detail.id, |
| | | value: detail.paramValue, |
| | |
| | | class="!w-full" |
| | | /> |
| | | </a-form-item> |
| | | <a-form-item label="æ¬æ¬¡æ¥å·¥æ°é" required> |
| | | <a-form-item v-if="waitDuration" label="æ¥å·¥å计æ¶"> |
| | | <WaitCountdown |
| | | :task-id="selectedTaskId" |
| | | :wait-duration="waitDuration" |
| | | :wait-start-time="waitStartTime" |
| | | /> |
| | | </a-form-item> |
| | | <a-form-item v-if="needReportQuantity" label="æ¬æ¬¡æ¥å·¥æ°é" required> |
| | | <a-input-number |
| | | v-model:value="formData.feedbackQuantity" |
| | | :min="0.01" |
| | |
| | | rules: z.number().default(CommonStatusEnum.ENABLE), |
| | | }, |
| | | { |
| | | fieldName: 'waitDuration', |
| | | label: 'çå¾
æ¶é¿(å°æ¶)', |
| | | component: 'InputNumber', |
| | | formItemClass: 'col-span-2', |
| | | componentProps: { |
| | | class: '!w-full', |
| | | min: 0, |
| | | precision: 2, |
| | | placeholder: 'ç空æ 0 表示æ éå计æ¶', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'needReportQuantity', |
| | | label: 'é填忥工æ°é', |
| | | component: 'Switch', |
| | | formItemClass: 'col-span-1', |
| | | defaultValue: true, |
| | | componentProps: { |
| | | checkedChildren: 'æ¯', |
| | | unCheckedChildren: 'å¦', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'attention', |
| | | label: 'å·¥åºè¯´æ', |
| | | component: 'Textarea', |
| | |
| | | modalApi.lock(); |
| | | try { |
| | | formData.value = await getProcess(data.id); |
| | | // è®¾ç½®å° values |
| | | await formApi.setValues(formData.value); |
| | | // è®¾ç½®å° valuesï¼æ§æ°æ®æªé
ç½®æ¥å·¥æ°éå¼å
³æ¶æé»è®¤éè¦å¡«åï¼ |
| | | await formApi.setValues({ |
| | | ...formData.value, |
| | | needReportQuantity: formData.value.needReportQuantity ?? true, |
| | | }); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | |
| | | } |
| | | |
| | | /** å·¥èºè·¯çº¿å·¥åºæç»è¡¨å */ |
| | | export function useRouteProcessFormSchema(processOptions: Array<{ label: string; value: number }>): VbenFormSchema[] { |
| | | export function useRouteProcessFormSchema( |
| | | processOptions: Array<{ label: string; value: number }>, |
| | | onProcessChange?: (processId?: number) => void, |
| | | ): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: "id", |
| | |
| | | options: processOptions, |
| | | placeholder: "è¯·éæ©å·¥åº", |
| | | showSearch: true, |
| | | onChange: onProcessChange, |
| | | }, |
| | | rules: "selectRequired", |
| | | }, |
| | |
| | | }, |
| | | { |
| | | fieldName: "waitTime", |
| | | label: "çå¾
æ¶é´(å)", |
| | | label: "çå¾
æ¶é¿(å°æ¶)", |
| | | component: "InputNumber", |
| | | componentProps: { |
| | | class: "!w-full", |
| | | min: 0, |
| | | precision: 0, |
| | | precision: 2, |
| | | }, |
| | | rules: z.number().default(0), |
| | | }, |
| | |
| | | width: 160, |
| | | }, |
| | | { field: "prepareTime", title: "å夿¶é´(å)", width: 110 }, |
| | | { field: "waitTime", title: "çå¾
æ¶é´(å)", width: 110 }, |
| | | { field: "waitTime", title: "çå¾
æ¶é¿(å°æ¶)", width: 110 }, |
| | | { |
| | | field: "colorCode", |
| | | title: "çç¹å¾é¢è²", |
| | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { getProcessSimpleList } from '#/api/mes/pro/process'; |
| | | import { getProcess, getProcessSimpleList } from '#/api/mes/pro/process'; |
| | | import { |
| | | createRouteProcess, |
| | | getRouteProcess, |
| | |
| | | wrapperClass: 'grid-cols-2', |
| | | }); |
| | | |
| | | /** 鿩工åºåï¼èªå¨å¸¦åºè¯¥å·¥åºé
ç½®ççå¾
æ¶é¿ï¼ç´æ¥å¡«å°æ¶æ°ï¼ä¸æ¢ç®ï¼ */ |
| | | async function handleProcessChange(processId?: number) { |
| | | if (!processId) { |
| | | return; |
| | | } |
| | | try { |
| | | const process = await getProcess(processId); |
| | | await formApi.setFieldValue('waitTime', process.waitDuration ?? 0); |
| | | } catch { |
| | | // å·¥åºè¯¦æ
è·å失败æ¶å¿½ç¥åæ¾ |
| | | } |
| | | } |
| | | |
| | | /** å 载工åºé项ååçæ schemaï¼é¿å
ä¸æç©ºéé¡¹ï¼ */ |
| | | async function loadSchema(): Promise<VbenFormSchema[]> { |
| | | const list = await getProcessSimpleList(); |
| | |
| | | label: item.name ?? '', |
| | | value: item.id!, |
| | | })); |
| | | return useRouteProcessFormSchema(options); |
| | | return useRouteProcessFormSchema(options, handleProcessChange); |
| | | } |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | |
| | | updateFeedback, |
| | | } from '#/api/mes/pro/feedback'; |
| | | import { getRouteProcessByRouteAndProcess } from '#/api/mes/pro/route/process'; |
| | | import { getTask } from '#/api/mes/pro/task'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useFormSchema } from '../../../pro/feedback/data'; |
| | | import { |
| | | loadParamDetails, |
| | | paramDetails, |
| | | resetWaitConfig, |
| | | waitDuration, |
| | | waitStartTime, |
| | | waitTaskId, |
| | | } from '../../../pro/feedback/param-state'; |
| | | import BatchInputDialog from '../../../pro/feedback/modules/batch-input-dialog.vue'; |
| | | import WaitCountdown from '../../../pro/feedback/modules/wait-countdown.vue'; |
| | | const batchDialogRef = ref<InstanceType<typeof BatchInputDialog>>(); |
| | | const batchActionType = ref<'' | 'submit' | 'approve'>(''); |
| | | const pendingSubmitMode = ref<'saveAndSubmit' | 'submit'>('submit'); |
| | |
| | | |
| | | /** æäº¤å坹齿°éï¼æ¥å·¥åªå¡«åæ¥å·¥æ°éï¼åæ ¼/ä¸è¯/åºåæ°éç±è´¨æ£ç¡®å®ï¼æ¥å·¥ç¯èä¸éé */ |
| | | function alignQuantity(data: any) { |
| | | if (data.needReportQuantity === false) { |
| | | data.feedbackQuantity = 0; |
| | | } |
| | | data.uncheckQuantity = data.checkFlag ? data.feedbackQuantity : 0; |
| | | delete data.qualifiedQuantity; |
| | | delete data.unqualifiedQuantity; |
| | | delete data.laborScrapQuantity; |
| | | delete data.materialScrapQuantity; |
| | | delete data.otherScrapQuantity; |
| | | delete data.needReportQuantity; |
| | | } |
| | | |
| | | async function getAlignedData() { |
| | |
| | | })); |
| | | } |
| | | |
| | | /** å è½½ checkFlag */ |
| | | async function resolveCheckFlag(routeId?: number, processId?: number) { |
| | | if (!routeId || !processId) return true; |
| | | /** å 载工åºé
ç½®ï¼checkFlag / çå¾
æ¶é¿ / æ¥å·¥æ°éè§åï¼ */ |
| | | async function resolveRouteProcessConfig(routeId?: number, processId?: number) { |
| | | if (!routeId || !processId) { |
| | | return { checkFlag: true, needReportQuantity: true }; |
| | | } |
| | | try { |
| | | const rp = await getRouteProcessByRouteAndProcess(routeId, processId); |
| | | return rp?.checkFlag ?? false; |
| | | waitDuration.value = rp?.waitDuration; |
| | | return { |
| | | checkFlag: rp?.checkFlag ?? false, |
| | | needReportQuantity: rp?.needReportQuantity ?? true, |
| | | }; |
| | | } catch { |
| | | return true; |
| | | waitDuration.value = undefined; |
| | | return { checkFlag: true, needReportQuantity: true }; |
| | | } |
| | | } |
| | | |
| | |
| | | feedbackId.value = undefined; |
| | | feedbackStatus.value = undefined; |
| | | originalSnapshot.value = ''; |
| | | resetWaitConfig(); |
| | | |
| | | formApi.setState({ schema: useFormSchema(formType.value, formApi) }); |
| | | |
| | | const code = await generateAutoCode(MesAutoCodeRuleCode.PRO_FEEDBACK_CODE); |
| | | const checkFlag = await resolveCheckFlag(task.routeId, task.processId); |
| | | const config = await resolveRouteProcessConfig(task.routeId, task.processId); |
| | | // å 载任å¡ç»å®çå·¥èºåæ°ï¼å¿
å¡«åæ°æ ¡éª + åæ°å¼åæ¾ï¼ |
| | | await loadParamDetails(task.id); |
| | | |
| | | waitTaskId.value = task.id; |
| | | waitStartTime.value = task.waitStartTime; |
| | | |
| | | await formApi.setValues({ |
| | | code, |
| | |
| | | reportableQuantity: task.reportableQuantity, |
| | | scheduledQuantity: task.quantity, |
| | | feedbackApprove: task.feedbackApprove, |
| | | checkFlag, |
| | | checkFlag: config.checkFlag, |
| | | needReportQuantity: config.needReportQuantity, |
| | | }); |
| | | originalSnapshot.value = JSON.stringify(await getAlignedData()); |
| | | } |
| | |
| | | feedbackStatus.value = data.status; |
| | | formType.value = 'update'; |
| | | formApi.setState({ schema: useFormSchema(formType.value, formApi) }); |
| | | const checkFlag = await resolveCheckFlag(data.routeId, data.processId); |
| | | const config = await resolveRouteProcessConfig(data.routeId, data.processId); |
| | | await loadParamDetails(data.taskId); |
| | | await formApi.setValues({ ...data, checkFlag }); |
| | | waitTaskId.value = data.taskId; |
| | | waitStartTime.value = undefined; |
| | | if (data.taskId) { |
| | | try { |
| | | const task = await getTask(data.taskId); |
| | | waitStartTime.value = task?.waitStartTime; |
| | | } catch { |
| | | // ä»»å¡ä¸å卿¶å¿½ç¥å计æ¶åæ¾ |
| | | } |
| | | } |
| | | await formApi.setValues({ |
| | | ...data, |
| | | checkFlag: config.checkFlag, |
| | | needReportQuantity: config.needReportQuantity, |
| | | }); |
| | | originalSnapshot.value = JSON.stringify(await getAlignedData()); |
| | | } |
| | | |
| | |
| | | |
| | | <Form class="feedback-tab__form" /> |
| | | |
| | | <!-- æ¥å·¥å计æ¶ï¼å·¥åºé
ç½®çå¾
æ¶é¿æ¶å±ç¤ºï¼ --> |
| | | <div v-if="waitDuration" class="feedback-tab__params"> |
| | | <div class="feedback-tab__params-title">æ¥å·¥å计æ¶</div> |
| | | <WaitCountdown |
| | | :task-id="waitTaskId" |
| | | :wait-duration="waitDuration" |
| | | :wait-start-time="waitStartTime" |
| | | :disabled="!isEditable" |
| | | /> |
| | | </div> |
| | | |
| | | <!-- å·¥èºåæ°ï¼ä»»å¡ç»å®åæ°æ¶å±ç¤ºï¼å¿
å¡«åæ°æªå¡«ä¼è¢«åç«¯æ¦æªï¼ --> |
| | | <div v-if="paramDetails.length" class="feedback-tab__params"> |
| | | <div class="feedback-tab__params-title">å·¥èºåæ°</div> |
| | |
| | | }, |
| | | }, |
| | | { |
| | | field: 'feedbackId', |
| | | title: 'æ¥æº', |
| | | width: 100, |
| | | slots: { default: 'source' }, |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | | width: 240, |
| | | fixed: 'right', |
| | |
| | | width: 100, |
| | | }, |
| | | { |
| | | field: 'qualityStatus', |
| | | title: 'è´¨éç¶æ', |
| | | width: 100, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.MES_WM_QUALITY_STATUS }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'batchCode', |
| | | title: 'æ¹æ¬¡å·', |
| | | minWidth: 120, |
| | | }, |
| | | ...(stockable |
| | | ? [ |
| | | { |
| | | field: 'isStock', |
| | | title: 'æ¯å¦å
¥åº', |
| | | width: 100, |
| | | slots: { default: 'isStock' }, |
| | | } as const, |
| | | ] |
| | | : []), |
| | | ...(editable || stockable |
| | | ? [ |
| | | { |
| | |
| | | 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.feedbackId" color="blue">èªå¨çæ</Tag> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | |
| | | <template v-if="formData?.id"> |
| | | <Divider>ç©æä¿¡æ¯</Divider> |
| | | <div class="mx-4"> |
| | | <LineList :form-type="formType" :receipt-id="formData.id" /> |
| | | <LineList |
| | | :form-type="formType" |
| | | :receipt-id="formData.id" |
| | | :auto-generated="!!formData.feedbackId" |
| | | /> |
| | | </div> |
| | | </template> |
| | | <template #prepend-footer> |
| | |
| | | import type { FormType } from '../data'; |
| | | |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { MesWmProductReceiptApi } from '#/api/mes/wm/productreceipt'; |
| | | import type { MesWmProductReceiptDetailApi } from '#/api/mes/wm/productreceipt/detail'; |
| | | import type { MesWmProductReceiptLineApi } from '#/api/mes/wm/productreceipt/line'; |
| | | |
| | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { BarcodeBizTypeEnum } from '@vben/constants'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | import { Button, Checkbox, message } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { submitProductReceiptStock } from '#/api/mes/wm/productreceipt'; |
| | | import { getProductReceiptDetailListByLineId } from '#/api/mes/wm/productreceipt/detail'; |
| | | import { |
| | | deleteProductReceiptLine, |
| | |
| | | const props = defineProps<{ |
| | | formType: FormType; |
| | | receiptId: number; |
| | | autoGenerated?: boolean; // æ¯å¦ç±æ¥å·¥èªå¨çæï¼èªå¨çæçè¡ä¸å¯æå·¥å¢å æ¹ï¼ |
| | | }>(); |
| | | |
| | | const isEditable = computed(() => |
| | | // æ¯å¦å¯ç¼è¾æç»è¡ |
| | | ['create', 'update'].includes(props.formType), |
| | | ); |
| | | const canEditLine = computed( |
| | | // èªå¨çæçå
¥åºåè¡ä¸å¯æå·¥å¢å æ¹ |
| | | () => isEditable.value && !props.autoGenerated, |
| | | ); |
| | | const isStock = computed(() => props.formType === 'stock'); // æ¯å¦ä¸ºä¸æ¶æ¨¡å¼ |
| | | const detailMap = reactive< |
| | | Record<number, MesWmProductReceiptDetailApi.ProductReceiptDetail[]> |
| | | >({}); // å·²å±å¼è¡ç䏿¶æç»ç¼å |
| | | const checkedMap = reactive<Record<number, boolean>>({}); // è¡æ¯å¦å
¥åºå¾éç¶æç¼å |
| | | const barcodeDetailRef = ref(); // æ¡ç 详æ
å¼¹çªå®ä¾ |
| | | |
| | | const [LineFormModal, lineFormModalApi] = useVbenModal({ |
| | |
| | | function handleRefresh() { |
| | | for (const id of Object.keys(detailMap)) { |
| | | delete detailMap[Number(id)]; |
| | | } |
| | | for (const id of Object.keys(checkedMap)) { |
| | | delete checkedMap[Number(id)]; |
| | | } |
| | | gridApi.query(); |
| | | } |
| | |
| | | row.itemCode, |
| | | row.itemName, |
| | | ); |
| | | } |
| | | |
| | | /** è¡æ¯å¦å
¥åºå¾éç¶æï¼æªæå¨å¾éæ¶åéå°è¡èªå¸¦å¼ï¼ */ |
| | | function isLineStock(row: MesWmProductReceiptLineApi.ProductReceiptLine) { |
| | | return checkedMap[row.id!] ?? row.isStock ?? false; |
| | | } |
| | | |
| | | /** åæ¢è¡æ¯å¦å
¥åº */ |
| | | function setLineStock( |
| | | row: MesWmProductReceiptLineApi.ProductReceiptLine, |
| | | checked: boolean, |
| | | ) { |
| | | checkedMap[row.id!] = checked; |
| | | } |
| | | |
| | | /** ä¿å䏿¶åé
ï¼æ¶éææè¡çå¾éç¶æä¸ä»åº/åºåº/åºä½åé
ï¼è°ç¨æ¹é䏿¶æ¥å£ */ |
| | | async function handleSaveStock() { |
| | | if (!props.receiptId) { |
| | | return; |
| | | } |
| | | const hideLoading = message.loading({ |
| | | content: 'æ£å¨ä¿å䏿¶åé
...', |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | const { list } = await getProductReceiptLinePage({ |
| | | pageNo: 1, |
| | | pageSize: 10000, |
| | | receiptId: props.receiptId, |
| | | }); |
| | | const rows: MesWmProductReceiptApi.StockRow[] = []; |
| | | for (const line of list) { |
| | | const isStock = isLineStock(line); |
| | | const row: MesWmProductReceiptApi.StockRow = { |
| | | lineId: line.id!, |
| | | isStock, |
| | | }; |
| | | if (isStock) { |
| | | let details = detailMap[line.id!]; |
| | | if (!details) { |
| | | details = await getProductReceiptDetailListByLineId(line.id!); |
| | | detailMap[line.id!] = details; |
| | | } |
| | | row.details = details.map((detail) => ({ |
| | | warehouseId: detail.warehouseId!, |
| | | locationId: detail.locationId!, |
| | | areaId: detail.areaId!, |
| | | quantity: detail.quantity!, |
| | | })); |
| | | } |
| | | rows.push(row); |
| | | } |
| | | await submitProductReceiptStock({ receiptId: props.receiptId, rows }); |
| | | message.success('䏿¶åé
ä¿åæå'); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | /** æå¼ä¸æ¶æç»è¡¨å */ |
| | |
| | | <LineFormModal @success="handleRefresh" /> |
| | | <DetailFormModal @success="handleDetailSuccess" /> |
| | | <Grid table-title="ç©æä¿¡æ¯"> |
| | | <template v-if="isEditable" #toolbar-tools> |
| | | <template v-if="canEditLine" #toolbar-tools> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | |
| | | ]" |
| | | /> |
| | | </template> |
| | | <template v-if="isStock" #toolbar-tools> |
| | | <Button type="primary" @click="handleSaveStock">ä¿å䏿¶åé
</Button> |
| | | </template> |
| | | <template #detail="{ row }"> |
| | | <DetailList |
| | | :details="getExpandedDetails(row)" |
| | |
| | | @edit="(detailId) => openDetailForm(row.id!, row.itemId, detailId)" |
| | | @refresh="loadLineDetails(row.id!)" |
| | | /> |
| | | </template> |
| | | <template #isStock="{ row }"> |
| | | <div class="flex justify-center"> |
| | | <Checkbox |
| | | :checked="isLineStock(row)" |
| | | @update:checked="(checked: boolean) => setLineStock(row, checked)" |
| | | /> |
| | | </div> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <div class="flex items-center justify-center"> |
| | |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | ifShow: isEditable, |
| | | ifShow: canEditLine, |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | ifShow: isEditable, |
| | | ifShow: canEditLine, |
| | | popConfirm: { |
| | | title: $t('ui.actionMessage.deleteConfirm', [row.itemName]), |
| | | confirm: handleDelete.bind(null, row), |