From e313de09c41d6c637dfb67e55e3ca878ec636998 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 30 May 2026 09:35:09 +0800 Subject: [PATCH] =?UTF-8?q?feat(mes):=20=E4=BC=98=E5=8C=96=20materialstock?= =?UTF-8?q?=20=E7=9A=84=E4=BB=A3=E7=A0=81=E5=AE=9E=E7=8E=B0=E9=A3=8E?= =?UTF-8?q?=E6=A0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web-antd/src/views/mes/pro/card/data.ts | 420 +++++++++++++++- .../views/mes/pro/task/components/index.ts | 1 + apps/web-antd/src/views/mes/pro/task/data.ts | 451 ++++++++++++++++- .../mes/pro/workorder/components/index.ts | 1 + .../components/pro-work-order-select.vue | 188 ++++---- .../views/mes/wm/batch/components/index.ts | 1 + apps/web-antd/src/views/mes/wm/batch/data.ts | 140 +++++- .../wm-material-stock-select-dialog.vue | 126 +++-- .../components/wm-material-stock-select.vue | 6 +- .../src/views/mes/wm/materialstock/data.ts | 12 +- .../src/views/mes/wm/materialstock/index.vue | 27 +- apps/web-ele/src/router/routes/modules/mes.ts | 9 + .../views/mes/pro/task/components/index.ts | 1 + apps/web-ele/src/views/mes/pro/task/data.ts | 454 +++++++++++++++++- .../views/mes/wm/batch/components/index.ts | 1 + .../wm-material-stock-select-dialog.vue | 130 +++-- .../components/wm-material-stock-select.vue | 6 +- .../src/views/mes/wm/materialstock/data.ts | 11 +- .../src/views/mes/wm/materialstock/index.vue | 32 +- pnpm-lock.yaml | 49 +- 20 files changed, 1800 insertions(+), 266 deletions(-) diff --git a/apps/web-antd/src/views/mes/pro/card/data.ts b/apps/web-antd/src/views/mes/pro/card/data.ts index e4e0d6f9a..b9da83a07 100644 --- a/apps/web-antd/src/views/mes/pro/card/data.ts +++ b/apps/web-antd/src/views/mes/pro/card/data.ts @@ -1,11 +1,427 @@ -import type { VbenFormSchema } from '#/adapter/form'; +import type { VbenFormApi, VbenFormSchema } from '#/adapter/form'; import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { MesProCardApi } from '#/api/mes/pro/card'; +import type { MesProCardProcessApi } from '#/api/mes/pro/card/process'; -import { markRaw } from 'vue'; +import { h, markRaw } from 'vue'; +import { DICT_TYPE } from '@vben/constants'; + +import { Button } from 'ant-design-vue'; + +import { generateAutoCode } from '#/api/mes/md/autocode/record'; import { MdItemSelect } from '#/views/mes/md/item/components'; +import { MdWorkstationSelect } from '#/views/mes/md/workstation/components'; +import { ProProcessSelect } from '#/views/mes/pro/process/components'; import { ProWorkOrderSelect } from '#/views/mes/pro/workorder/components'; +import { + MesAutoCodeRuleCode, + MesProWorkOrderStatusEnum, +} from '#/views/mes/utils/constants'; +import { UserSelect } from '#/views/system/user/components'; + +/** 表单类型 */ +export type FormType = 'create' | 'detail' | 'finish' | 'update'; + +/** 表头是否只读(完成、详情态) */ +function isHeaderReadonly(formType: FormType): boolean { + return formType === 'detail' || formType === 'finish'; +} + +/** 新增/修改的表单 */ +export function useFormSchema( + formType: FormType, + formApi?: VbenFormApi, +): VbenFormSchema[] { + const headerReadonly = isHeaderReadonly(formType); + return [ + { + fieldName: 'id', + component: 'Input', + dependencies: { + triggerFields: [''], + show: () => false, + }, + }, + { + fieldName: 'status', + component: 'Input', + dependencies: { + triggerFields: [''], + show: () => false, + }, + }, + { + fieldName: 'code', + label: '流转卡编码', + component: 'Input', + componentProps: { + disabled: headerReadonly, + placeholder: '请输入流转卡编码', + }, + rules: 'required', + suffix: + formType === 'create' || formType === 'update' + ? () => + h( + Button, + { + type: 'default', + onClick: async () => { + const code = await generateAutoCode( + MesAutoCodeRuleCode.PRO_CARD_CODE, + ); + await formApi?.setFieldValue('code', code); + }, + }, + { default: () => '生成' }, + ) + : undefined, + }, + { + fieldName: 'workOrderId', + label: '生产工单', + component: markRaw(ProWorkOrderSelect), + componentProps: { + disabled: headerReadonly, + placeholder: '请选择生产工单', + status: MesProWorkOrderStatusEnum.CONFIRMED, + }, + rules: 'selectRequired', + }, + { + fieldName: 'itemId', + label: '产品', + component: markRaw(MdItemSelect), + componentProps: { + disabled: headerReadonly, + placeholder: '请选择产品', + }, + rules: 'selectRequired', + }, + { + fieldName: 'transferedQuantity', + label: '流转数量', + component: 'InputNumber', + componentProps: { + class: '!w-full', + disabled: headerReadonly, + min: 0, + placeholder: '请输入流转数量', + precision: 2, + }, + rules: 'required', + }, + { + fieldName: 'batchCode', + label: '批次号', + component: 'Input', + componentProps: { + disabled: headerReadonly, + placeholder: '请输入批次号', + }, + }, + { + fieldName: 'remark', + label: '备注', + component: 'Textarea', + formItemClass: 'col-span-3', + componentProps: { + disabled: headerReadonly, + placeholder: '请输入备注', + rows: 3, + }, + }, + ]; +} + +/** 列表的搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'code', + label: '流转卡编码', + component: 'Input', + componentProps: { + allowClear: true, + placeholder: '请输入流转卡编码', + }, + }, + { + fieldName: 'workOrderId', + label: '生产工单', + component: markRaw(ProWorkOrderSelect), + componentProps: { + placeholder: '请选择生产工单', + }, + }, + { + fieldName: 'itemId', + label: '产品', + component: markRaw(MdItemSelect), + componentProps: { + placeholder: '请选择产品', + }, + }, + { + fieldName: 'batchCode', + label: '批次号', + component: 'Input', + componentProps: { + allowClear: true, + placeholder: '请输入批次号', + }, + }, + ]; +} + +/** 列表的字段 */ +export function useGridColumns(): VxeTableGridOptions['columns'] { + return [ + { + field: 'code', + title: '流转卡编码', + width: 160, + slots: { default: 'code' }, + }, + { + field: 'workOrderCode', + title: '生产工单编号', + width: 160, + }, + { + field: 'workOrderName', + title: '工单名称', + minWidth: 150, + }, + { + field: 'batchCode', + title: '批次号', + width: 120, + }, + { + field: 'itemCode', + title: '产品物料编码', + width: 140, + }, + { + field: 'itemName', + title: '产品物料名称', + minWidth: 120, + }, + { + field: 'specification', + title: '规格型号', + width: 120, + }, + { + field: 'unitMeasureName', + title: '单位', + width: 80, + }, + { + field: 'transferedQuantity', + title: '流转数量', + width: 100, + }, + { + field: 'status', + title: '单据状态', + minWidth: 100, + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.MES_PRO_WORK_ORDER_STATUS }, + }, + }, + { + title: '操作', + width: 240, + fixed: 'right', + slots: { default: 'actions' }, + }, + ]; +} + +/** 工序记录子表的字段 */ +export function useProcessGridColumns( + editable: boolean, +): VxeTableGridOptions['columns'] { + return [ + { + field: 'sort', + title: '序号', + width: 60, + }, + { + field: 'processName', + title: '工序名称', + minWidth: 120, + }, + { + field: 'processCode', + title: '工序编码', + width: 120, + }, + { + field: 'inputTime', + title: '进入工序时间', + width: 180, + formatter: 'formatDateTime', + }, + { + field: 'outputTime', + title: '出工序时间', + width: 180, + formatter: 'formatDateTime', + }, + { + field: 'inputQuantity', + title: '投入数量', + width: 100, + }, + { + field: 'outputQuantity', + title: '产出数量', + width: 100, + }, + { + field: 'unqualifiedQuantity', + title: '不良品数量', + width: 100, + }, + { + field: 'workstationCode', + title: '工位编码', + width: 120, + }, + { + field: 'workstationName', + title: '工位名称', + minWidth: 120, + }, + { + field: 'nickname', + title: '操作人', + width: 100, + }, + ...(editable + ? [ + { + title: '操作', + width: 160, + fixed: 'right', + slots: { default: 'actions' }, + } as const, + ] + : []), + ]; +} + +/** 工序记录新增/修改的表单 */ +export function useProcessFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'sort', + label: '序号', + component: 'InputNumber', + componentProps: { + class: '!w-full', + min: 0, + placeholder: '请输入序号', + precision: 0, + }, + }, + { + fieldName: 'processId', + label: '工序', + component: markRaw(ProProcessSelect), + componentProps: { + placeholder: '请选择工序', + }, + }, + { + fieldName: 'inputTime', + label: '进入工序时间', + component: 'DatePicker', + componentProps: { + class: '!w-full', + placeholder: '请选择进入工序时间', + showTime: true, + valueFormat: 'x', + }, + }, + { + fieldName: 'outputTime', + label: '出工序时间', + component: 'DatePicker', + componentProps: { + class: '!w-full', + placeholder: '请选择出工序时间', + showTime: true, + valueFormat: 'x', + }, + }, + { + fieldName: 'inputQuantity', + label: '投入数量', + component: 'InputNumber', + componentProps: { + class: '!w-full', + min: 0, + placeholder: '请输入投入数量', + precision: 2, + }, + }, + { + fieldName: 'outputQuantity', + label: '产出数量', + component: 'InputNumber', + componentProps: { + class: '!w-full', + min: 0, + placeholder: '请输入产出数量', + precision: 2, + }, + }, + { + fieldName: 'unqualifiedQuantity', + label: '不合格数量', + component: 'InputNumber', + componentProps: { + class: '!w-full', + min: 0, + placeholder: '请输入不合格数量', + precision: 2, + }, + }, + { + fieldName: 'workstationId', + label: '工位', + component: markRaw(MdWorkstationSelect), + componentProps: { + placeholder: '请选择工位', + }, + }, + { + fieldName: 'userId', + label: '操作人', + component: markRaw(UserSelect), + componentProps: { + placeholder: '请选择操作人', + }, + }, + { + fieldName: 'remark', + label: '备注', + component: 'Textarea', + formItemClass: 'col-span-2', + componentProps: { + placeholder: '请输入备注', + rows: 3, + }, + }, + ]; +} /** 流转卡选择弹窗的搜索表单 */ export function useCardSelectGridFormSchema(): VbenFormSchema[] { diff --git a/apps/web-antd/src/views/mes/pro/task/components/index.ts b/apps/web-antd/src/views/mes/pro/task/components/index.ts index 3847bb912..bfaf6ae17 100644 --- a/apps/web-antd/src/views/mes/pro/task/components/index.ts +++ b/apps/web-antd/src/views/mes/pro/task/components/index.ts @@ -1,2 +1,3 @@ +export { default as GanttChart } from './gantt-chart.vue'; export { default as ProTaskSelectDialog } from './pro-task-select-dialog.vue'; export { default as ProTaskSelect } from './pro-task-select.vue'; diff --git a/apps/web-antd/src/views/mes/pro/task/data.ts b/apps/web-antd/src/views/mes/pro/task/data.ts index c29921b81..baede0e30 100644 --- a/apps/web-antd/src/views/mes/pro/task/data.ts +++ b/apps/web-antd/src/views/mes/pro/task/data.ts @@ -1,15 +1,464 @@ -import type { VbenFormSchema } from '#/adapter/form'; +import type { VbenFormApi, VbenFormSchema } from '#/adapter/form'; import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { MesProTaskApi } from '#/api/mes/pro/task'; +import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder'; import { markRaw } from 'vue'; import { DICT_TYPE } from '@vben/constants'; +import { getDictOptions } from '@vben/hooks'; +import { getRangePickerDefaultProps } from '#/utils'; +import MdClientSelect from '#/views/mes/md/client/components/md-client-select.vue'; +import { MdItemSelect } from '#/views/mes/md/item/components'; import { MdWorkstationSelect } from '#/views/mes/md/workstation/components'; import { ProProcessSelect } from '#/views/mes/pro/process/components'; +import { RouteColorPicker } from '#/views/mes/pro/route/components'; import { ProWorkOrderSelect } from '#/views/mes/pro/workorder/components'; +/** 待排产工单列表的搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'code', + label: '工单编码', + component: 'Input', + componentProps: { + allowClear: true, + placeholder: '请输入工单编码', + }, + }, + { + fieldName: 'name', + label: '工单名称', + component: 'Input', + componentProps: { + allowClear: true, + placeholder: '请输入工单名称', + }, + }, + { + fieldName: 'orderSourceCode', + label: '来源单据', + component: 'Input', + componentProps: { + allowClear: true, + placeholder: '请输入来源单据编号', + }, + }, + { + fieldName: 'productId', + label: '产品', + component: markRaw(MdItemSelect), + componentProps: { + placeholder: '请选择产品', + }, + }, + { + fieldName: 'clientId', + label: '客户', + component: markRaw(MdClientSelect), + componentProps: { + placeholder: '请选择客户', + }, + }, + { + fieldName: 'requestDate', + label: '需求日期', + component: 'RangePicker', + componentProps: { + ...getRangePickerDefaultProps(), + allowClear: true, + }, + }, + ]; +} + +/** 待排产工单列表的字段 */ +export function useGridColumns(): VxeTableGridOptions['columns'] { + return [ + { + field: 'code', + title: '工单编码', + fixed: 'left', + width: 200, + treeNode: true, + slots: { default: 'code' }, + }, + { + field: 'name', + title: '工单名称', + minWidth: 150, + }, + { + field: 'orderSourceType', + title: '工单来源', + width: 100, + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.MES_PRO_WORK_ORDER_SOURCE_TYPE }, + }, + }, + { + field: 'orderSourceCode', + title: '来源单据编号', + width: 140, + }, + { + field: 'productCode', + title: '产品编码', + width: 120, + }, + { + field: 'productName', + title: '产品名称', + minWidth: 120, + }, + { + field: 'productSpecification', + title: '规格型号', + width: 120, + }, + { + field: 'unitMeasureName', + title: '单位', + width: 80, + }, + { + field: 'quantity', + title: '工单数量', + width: 100, + }, + { + field: 'quantityChanged', + title: '调整数量', + width: 100, + }, + { + field: 'quantityProduced', + title: '已生产数量', + width: 100, + }, + { + field: 'clientCode', + title: '客户编码', + width: 120, + }, + { + field: 'clientName', + title: '客户名称', + width: 120, + }, + { + field: 'requestDate', + title: '需求日期', + width: 120, + formatter: 'formatDate', + }, + { + field: 'status', + title: '排产状态', + width: 100, + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.MES_PRO_WORK_ORDER_STATUS }, + }, + }, + { + title: '操作', + width: 100, + fixed: 'right', + slots: { default: 'actions' }, + }, + ]; +} + +/** 排产对话框只读工单信息的表单 */ +export function useScheduleFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'code', + label: '工单编码', + component: 'Input', + componentProps: { + disabled: true, + }, + }, + { + fieldName: 'name', + label: '工单名称', + component: 'Input', + componentProps: { + disabled: true, + }, + }, + { + fieldName: 'orderSourceType', + label: '工单来源', + component: 'Select', + componentProps: { + disabled: true, + options: getDictOptions(DICT_TYPE.MES_PRO_WORK_ORDER_SOURCE_TYPE, 'number'), + }, + }, + { + fieldName: 'orderSourceCode', + label: '来源单据编号', + component: 'Input', + componentProps: { + disabled: true, + }, + }, + { + fieldName: 'type', + label: '工单类型', + component: 'Select', + componentProps: { + disabled: true, + options: getDictOptions(DICT_TYPE.MES_PRO_WORK_ORDER_TYPE, 'number'), + }, + }, + { + fieldName: 'productId', + label: '产品', + component: markRaw(MdItemSelect), + componentProps: { + disabled: true, + }, + }, + { + fieldName: 'productSpecification', + label: '规格型号', + component: 'Input', + componentProps: { + disabled: true, + }, + }, + { + fieldName: 'unitMeasureName', + label: '单位', + component: 'Input', + componentProps: { + disabled: true, + }, + }, + { + fieldName: 'quantity', + label: '工单数量', + component: 'InputNumber', + componentProps: { + class: '!w-full', + disabled: true, + precision: 2, + }, + }, + { + fieldName: 'clientId', + label: '客户', + component: markRaw(MdClientSelect), + componentProps: { + disabled: true, + }, + }, + { + fieldName: 'batchCode', + label: '批次号', + component: 'Input', + componentProps: { + disabled: true, + }, + }, + { + fieldName: 'requestDate', + label: '需求日期', + component: 'DatePicker', + componentProps: { + class: '!w-full', + disabled: true, + format: 'YYYY-MM-DD', + valueFormat: 'x', + }, + }, + { + fieldName: 'remark', + label: '备注', + component: 'Textarea', + formItemClass: 'col-span-3', + componentProps: { + disabled: true, + rows: 2, + }, + }, + ]; +} + +/** 生产任务子表的字段 */ +export function useTaskGridColumns( + editable: boolean, +): VxeTableGridOptions['columns'] { + return [ + { + field: 'code', + title: '任务编码', + width: 140, + }, + { + field: 'name', + title: '任务名称', + minWidth: 150, + }, + { + field: 'workstationCode', + title: '工作站编号', + width: 120, + }, + { + field: 'workstationName', + title: '工作站名称', + width: 120, + }, + { + field: 'quantity', + title: '排产数量', + width: 100, + }, + { + field: 'producedQuantity', + title: '已生产数量', + width: 100, + }, + { + field: 'startTime', + title: '开始生产时间', + width: 170, + formatter: 'formatDateTime', + }, + { + field: 'duration', + title: '生产时长', + width: 80, + }, + { + field: 'endTime', + title: '预计完成时间', + width: 170, + formatter: 'formatDateTime', + }, + { + field: 'colorCode', + title: '显示颜色', + width: 100, + slots: { default: 'colorCode' }, + }, + ...(editable + ? [ + { + title: '操作', + width: 160, + fixed: 'right', + slots: { default: 'actions' }, + } as const, + ] + : []), + ]; +} + +/** 生产任务新增/修改的表单 */ +export function useTaskFormSchema(formApi?: VbenFormApi): VbenFormSchema[] { + return [ + { + fieldName: 'workstationId', + label: '工作站', + component: markRaw(MdWorkstationSelect), + componentProps: { + placeholder: '请选择工作站', + }, + rules: 'selectRequired', + }, + { + fieldName: 'quantity', + label: '排产数量', + component: 'InputNumber', + componentProps: { + class: '!w-full', + min: 0.01, + placeholder: '请输入排产数量', + precision: 2, + }, + rules: 'required', + }, + { + fieldName: 'colorCode', + label: '甘特颜色', + component: markRaw(RouteColorPicker), + }, + { + fieldName: 'startTime', + label: '开始时间', + component: 'DatePicker', + componentProps: { + class: '!w-full', + placeholder: '请选择开始时间', + showTime: true, + valueFormat: 'x', + // 开始时间变更:重新计算结束时间 + onChange: () => recalcEndTime(formApi), + }, + rules: 'required', + }, + { + fieldName: 'duration', + label: '生产时长', + component: 'InputNumber', + componentProps: { + class: '!w-full', + min: 1, + placeholder: '请输入生产时长', + precision: 0, + // 生产时长变更:重新计算结束时间 + onChange: () => recalcEndTime(formApi), + }, + rules: 'required', + }, + { + fieldName: 'endTime', + label: '结束时间', + component: 'DatePicker', + componentProps: { + class: '!w-full', + disabled: true, + showTime: true, + valueFormat: 'x', + }, + }, + { + fieldName: 'remark', + label: '备注', + component: 'Textarea', + formItemClass: 'col-span-3', + componentProps: { + placeholder: '请输入备注', + rows: 2, + }, + }, + ]; +} + +/** 计算结束时间:开始时间 + 生产时长 × 8 小时 */ +async function recalcEndTime(formApi?: VbenFormApi) { + if (!formApi) { + return; + } + const values = await formApi.getValues(); + if (values.startTime && values.duration) { + const start = Number(values.startTime); + await formApi.setFieldValue( + 'endTime', + start + values.duration * 8 * 3600 * 1000, + ); + } +} + /** 任务选择弹窗的搜索表单 */ export function useTaskSelectGridFormSchema(): VbenFormSchema[] { return [ diff --git a/apps/web-antd/src/views/mes/pro/workorder/components/index.ts b/apps/web-antd/src/views/mes/pro/workorder/components/index.ts index 1059f8a79..b53e2876b 100644 --- a/apps/web-antd/src/views/mes/pro/workorder/components/index.ts +++ b/apps/web-antd/src/views/mes/pro/workorder/components/index.ts @@ -1 +1,2 @@ +export { default as ProWorkOrderSelectDialog } from './pro-work-order-select-dialog.vue'; export { default as ProWorkOrderSelect } from './pro-work-order-select.vue'; diff --git a/apps/web-antd/src/views/mes/pro/workorder/components/pro-work-order-select.vue b/apps/web-antd/src/views/mes/pro/workorder/components/pro-work-order-select.vue index 8b3907b1a..fb3a53503 100644 --- a/apps/web-antd/src/views/mes/pro/workorder/components/pro-work-order-select.vue +++ b/apps/web-antd/src/views/mes/pro/workorder/components/pro-work-order-select.vue @@ -1,20 +1,16 @@ diff --git a/apps/web-antd/src/views/mes/wm/batch/components/index.ts b/apps/web-antd/src/views/mes/wm/batch/components/index.ts index 6d4a5d756..1b9294cdd 100644 --- a/apps/web-antd/src/views/mes/wm/batch/components/index.ts +++ b/apps/web-antd/src/views/mes/wm/batch/components/index.ts @@ -1,2 +1,3 @@ +export { default as WmBatchDetail } from './wm-batch-detail.vue'; export { default as WmBatchSelectDialog } from './wm-batch-select-dialog.vue'; export { default as WmBatchSelect } from './wm-batch-select.vue'; diff --git a/apps/web-antd/src/views/mes/wm/batch/data.ts b/apps/web-antd/src/views/mes/wm/batch/data.ts index 74b0e3776..285b55e5e 100644 --- a/apps/web-antd/src/views/mes/wm/batch/data.ts +++ b/apps/web-antd/src/views/mes/wm/batch/data.ts @@ -1,16 +1,21 @@ import type { VbenFormSchema } from '#/adapter/form'; import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { MesWmBatchApi } from '#/api/mes/wm/batch'; +import type { DescriptionItemSchema } from '#/components/description'; -import { markRaw } from 'vue'; +import { h, markRaw } from 'vue'; import { DICT_TYPE } from '@vben/constants'; import { getDictOptions } from '@vben/hooks'; +import { formatDate } from '@vben/utils'; +import { DictTag } from '#/components/dict-tag'; +import { getRangePickerDefaultProps } from '#/utils'; import MdClientSelect from '#/views/mes/md/client/components/md-client-select.vue'; import MdItemSelect from '#/views/mes/md/item/components/md-item-select.vue'; import MdVendorSelect from '#/views/mes/md/vendor/components/md-vendor-select.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 { TmToolSelect } from '#/views/mes/tm/tool/components'; @@ -66,6 +71,14 @@ export function useBatchSelectGridFormSchema(): VbenFormSchema[] { placeholder: '请选择工作站', }, }, + { + fieldName: 'taskId', + label: '生产任务', + component: markRaw(ProTaskSelect), + componentProps: { + placeholder: '请选择生产任务', + }, + }, { fieldName: 'toolId', label: '工具', @@ -74,6 +87,15 @@ export function useBatchSelectGridFormSchema(): VbenFormSchema[] { placeholder: '请选择工具', }, }, + { + fieldName: 'moldId', + label: '模具编号', + component: 'Input', + componentProps: { + allowClear: true, + placeholder: '请输入模具编号', + }, + }, { fieldName: 'salesOrderCode', label: '销售订单编号', @@ -111,6 +133,33 @@ export function useBatchSelectGridFormSchema(): VbenFormSchema[] { placeholder: '请选择质量状态', }, }, + { + fieldName: 'produceDate', + label: '生产日期', + component: 'RangePicker', + componentProps: { + ...getRangePickerDefaultProps(), + allowClear: true, + }, + }, + { + fieldName: 'expireDate', + label: '有效期', + component: 'RangePicker', + componentProps: { + ...getRangePickerDefaultProps(), + allowClear: true, + }, + }, + { + fieldName: 'receiptDate', + label: '入库日期', + component: 'RangePicker', + componentProps: { + ...getRangePickerDefaultProps(), + allowClear: true, + }, + }, ]; } @@ -232,3 +281,92 @@ export function useBatchSelectGridColumns( }, ]; } + +/** 批次详情的描述字段 */ +export function useDetailSchema(): DescriptionItemSchema[] { + return [ + { + field: 'code', + label: '批次编号', + }, + { + field: 'itemCode', + label: '物料编码', + }, + { + field: 'itemName', + label: '物料名称', + }, + { + field: 'itemSpecification', + label: '规格型号', + }, + { + field: 'unitName', + label: '单位', + }, + { + field: 'lotNumber', + label: '生产批号', + }, + { + field: 'produceDate', + label: '生产日期', + render: (value) => (value ? formatDate(value, 'YYYY-MM-DD') : '-'), + }, + { + field: 'expireDate', + label: '有效期', + render: (value) => (value ? formatDate(value, 'YYYY-MM-DD') : '-'), + }, + { + field: 'receiptDate', + label: '入库日期', + render: (value) => (value ? formatDate(value, 'YYYY-MM-DD') : '-'), + }, + { + field: 'vendorName', + label: '供应商', + render: (value) => value || '-', + }, + { + field: 'clientName', + label: '客户', + render: (value) => value || '-', + }, + { + field: 'workstationCode', + label: '工作站', + render: (value) => value || '-', + }, + { + field: 'purchaseOrderCode', + label: '采购订单编号', + render: (value) => value || '-', + }, + { + field: 'salesOrderCode', + label: '销售订单编号', + render: (value) => value || '-', + }, + { + field: 'workOrderCode', + label: '生产工单', + render: (value) => value || '-', + }, + { + field: 'qualityStatus', + label: '质量状态', + render: (value) => + value == null + ? '-' + : h(DictTag, { type: DICT_TYPE.MES_WM_QUALITY_STATUS, value }), + }, + { + field: 'remark', + label: '备注', + span: 3, + render: (value) => value || '-', + }, + ]; +} diff --git a/apps/web-antd/src/views/mes/wm/materialstock/components/wm-material-stock-select-dialog.vue b/apps/web-antd/src/views/mes/wm/materialstock/components/wm-material-stock-select-dialog.vue index 837983a1d..a6201fbc6 100644 --- a/apps/web-antd/src/views/mes/wm/materialstock/components/wm-material-stock-select-dialog.vue +++ b/apps/web-antd/src/views/mes/wm/materialstock/components/wm-material-stock-select-dialog.vue @@ -35,8 +35,7 @@ const emit = defineEmits<{ }>(); const open = ref(false); -const multiple = ref(true); -const syncingSingleSelection = ref(false); +const multiple = ref(false); // 是否多选;默认按单选选择器使用 const selectedRows = ref([]); const preSelectedIds = ref([]); const searchItemTypeId = ref(); @@ -64,50 +63,37 @@ const alertTitle = computed(() => { return `已按${parts.join('/')}预过滤`; }); -/** 单选模式同步 VXE 勾选状态 */ -async function syncSingleSelection(row?: MesWmMaterialStockApi.MaterialStock) { - syncingSingleSelection.value = true; - await nextTick(); - await gridApi.grid.clearCheckboxRow(); - if (row) { - await gridApi.grid.setCheckboxRow(row, true); - } - await nextTick(); - syncingSingleSelection.value = false; +/** 获取多选记录,包含 VXE reserve 跨页记录 */ +function getMultipleSelectedRows() { + const selectedMap = new Map(); + const records = [ + ...(gridApi.grid.getCheckboxReserveRecords?.() ?? []), + ...(gridApi.grid.getCheckboxRecords?.() ?? []), + ] as MesWmMaterialStockApi.MaterialStock[]; + records.forEach((row) => { + const rowId = row.id; + if (rowId !== undefined) { + selectedMap.set(rowId, row); + } + }); + return [...selectedMap.values()]; } -/** 处理勾选变化 */ -async function handleCheckboxChange({ - checked, - records, - row, -}: { - checked: boolean; - records: MesWmMaterialStockApi.MaterialStock[]; - row?: MesWmMaterialStockApi.MaterialStock; -}) { - if (syncingSingleSelection.value) { - return; - } - if (!multiple.value) { - const selected = checked && row ? [row] : []; - selectedRows.value = selected; - await syncSingleSelection(selected[0]); - return; - } - selectedRows.value = records; +/** 处理多选勾选变化 */ +function handleCheckboxSelectChange() { + selectedRows.value = getMultipleSelectedRows(); } -/** 处理全选变化 */ -function handleCheckboxAll({ - records, -}: { - records: MesWmMaterialStockApi.MaterialStock[]; -}) { - if (syncingSingleSelection.value) { - return; - } - selectedRows.value = records; +/** 处理单选切换 */ +function handleRadioChange(row: MesWmMaterialStockApi.MaterialStock) { + selectedRows.value = [row]; +} + +/** 多选模式下切换行勾选 */ +async function toggleMultipleRow(row: MesWmMaterialStockApi.MaterialStock) { + const selected = gridApi.grid.isCheckedByCheckboxRow(row); + await gridApi.grid.setCheckboxRow(row, !selected); + selectedRows.value = getMultipleSelectedRows(); } /** 双击行:单选直接确认;多选切换勾选 */ @@ -117,33 +103,34 @@ async function handleRowDblclick({ row: MesWmMaterialStockApi.MaterialStock; }) { if (multiple.value) { - const checked = !gridApi.grid.isCheckedByCheckboxRow(row); - await gridApi.grid.setCheckboxRow(row, checked); - handleCheckboxChange({ - checked, - records: gridApi.grid.getCheckboxRecords() as MesWmMaterialStockApi.MaterialStock[], - row, - }); + await toggleMultipleRow(row); return; } selectedRows.value = [row]; - await syncSingleSelection(row); + await gridApi.grid.setRadioRow(row); handleConfirm(); } /** 回显预选 */ -function applyPreSelection() { +async function applyPreSelection() { if (preSelectedIds.value.length === 0) { return; } const rows = gridApi.grid.getData() as MesWmMaterialStockApi.MaterialStock[]; for (const row of rows) { - if (row.id && preSelectedIds.value.includes(row.id)) { - gridApi.grid.setCheckboxRow(row, true); - if (!multiple.value) { - selectedRows.value = [row]; - } + if (row.id === undefined || !preSelectedIds.value.includes(row.id)) { + continue; } + if (multiple.value) { + await gridApi.grid.setCheckboxRow(row, true); + } else { + await gridApi.grid.setRadioRow(row); + selectedRows.value = [row]; + return; + } + } + if (multiple.value) { + selectedRows.value = getMultipleSelectedRows(); } } @@ -152,10 +139,11 @@ const [Grid, gridApi] = useVbenVxeGrid({ schema: useSelectGridFormSchema(), }, gridOptions: { - columns: useSelectGridColumns(), + columns: useSelectGridColumns(false), height: 480, keepSource: true, checkboxConfig: { highlight: true, range: true, reserve: true }, + radioConfig: { highlight: true, trigger: 'row' }, proxyConfig: { ajax: { query: async ({ page }, formValues) => { @@ -186,8 +174,11 @@ const [Grid, gridApi] = useVbenVxeGrid({ } as VxeTableGridOptions, gridEvents: { cellDblclick: handleRowDblclick, - checkboxAll: handleCheckboxAll, - checkboxChange: handleCheckboxChange, + checkboxAll: handleCheckboxSelectChange, + checkboxChange: handleCheckboxSelectChange, + radioChange: ({ row }: { row: MesWmMaterialStockApi.MaterialStock }) => { + handleRadioChange(row); + }, }, }); @@ -202,6 +193,8 @@ async function resetQueryState() { selectedRows.value = []; searchItemTypeId.value = undefined; await gridApi.grid.clearCheckboxRow(); + await gridApi.grid.clearCheckboxReserve(); + await gridApi.grid.clearRadioRow(); await gridApi.formApi.resetForm(); } @@ -211,13 +204,16 @@ async function openModal( options?: { multiple?: boolean }, ) { open.value = true; - multiple.value = options?.multiple ?? true; + multiple.value = options?.multiple ?? false; preSelectedIds.value = selectedIds || []; await nextTick(); + gridApi.setGridOptions({ + columns: useSelectGridColumns(multiple.value), + }); await resetQueryState(); await gridApi.query(); await nextTick(); - applyPreSelection(); + await applyPreSelection(); } /** 关闭弹窗 */ @@ -228,14 +224,12 @@ async function closeModal() { /** 确认选择 */ function handleConfirm() { - if (selectedRows.value.length === 0) { + const rows = multiple.value ? getMultipleSelectedRows() : selectedRows.value; + if (rows.length === 0) { message.warning(multiple.value ? '请至少选择一条数据' : '请选择一条数据'); return; } - emit( - 'selected', - multiple.value ? selectedRows.value : [selectedRows.value[0]!], - ); + emit('selected', multiple.value ? rows : [rows[0]!]); open.value = false; } diff --git a/apps/web-antd/src/views/mes/wm/materialstock/components/wm-material-stock-select.vue b/apps/web-antd/src/views/mes/wm/materialstock/components/wm-material-stock-select.vue index e8dd366c7..6430bee14 100644 --- a/apps/web-antd/src/views/mes/wm/materialstock/components/wm-material-stock-select.vue +++ b/apps/web-antd/src/views/mes/wm/materialstock/components/wm-material-stock-select.vue @@ -71,11 +71,7 @@ async function resolveItemById(id: number | undefined) { if (selectedItem.value?.id === id) { return; } - try { - selectedItem.value = await getMaterialStock(id); - } catch (error) { - console.error('[WmMaterialStockSelect] resolveItemById failed:', error); - } + selectedItem.value = await getMaterialStock(id); } watch(() => props.modelValue, resolveItemById, { immediate: true }); diff --git a/apps/web-antd/src/views/mes/wm/materialstock/data.ts b/apps/web-antd/src/views/mes/wm/materialstock/data.ts index 0500129ec..cde557f50 100644 --- a/apps/web-antd/src/views/mes/wm/materialstock/data.ts +++ b/apps/web-antd/src/views/mes/wm/materialstock/data.ts @@ -72,8 +72,12 @@ export function useGridFormSchema(): VbenFormSchema[] { } /** 列表的字段 */ +// TODO @AI:看看别的模块,是不是会叫 onFrozenChange?还是一般叫什么梗合适??? export function useGridColumns( - onFrozenChange: (row: MesWmMaterialStockApi.MaterialStock) => void, + onFrozenChange: ( + newFrozen: boolean, + row: MesWmMaterialStockApi.MaterialStock, + ) => Promise, ): VxeTableGridOptions['columns'] { return [ { @@ -206,10 +210,12 @@ export function useSelectGridFormSchema(): VbenFormSchema[] { } /** 选择弹窗的字段 */ -export function useSelectGridColumns(): VxeTableGridOptions['columns'] { +export function useSelectGridColumns( + multiple = false, +): VxeTableGridOptions['columns'] { return [ { - type: 'checkbox', + type: multiple ? 'checkbox' : 'radio', width: 50, }, { diff --git a/apps/web-antd/src/views/mes/wm/materialstock/index.vue b/apps/web-antd/src/views/mes/wm/materialstock/index.vue index d24ecaf3e..3006511ed 100644 --- a/apps/web-antd/src/views/mes/wm/materialstock/index.vue +++ b/apps/web-antd/src/views/mes/wm/materialstock/index.vue @@ -17,6 +17,7 @@ import { } from '#/api/mes/wm/materialstock'; import { $t } from '#/locales'; import MdItemTypeTree from '#/views/mes/md/item/type/components/md-item-type-tree.vue'; +import { WmBatchDetail } from '#/views/mes/wm/batch/components'; import AreaForm from '#/views/mes/wm/warehouse/area/modules/form.vue'; import { useGridColumns, useGridFormSchema } from './data'; @@ -26,6 +27,8 @@ const [AreaModal, areaModalApi] = useVbenModal({ destroyOnClose: true, }); +const batchDetailRef = ref>(); + /** 刷新表格 */ function handleRefresh() { gridApi.query(); @@ -55,11 +58,20 @@ function handleOpenAreaDetail(row: MesWmMaterialStockApi.MaterialStock) { areaModalApi.setData({ formType: 'detail', id: row.areaId }).open(); } +/** 打开批次详情弹窗 */ +function handleOpenBatchDetail(row: MesWmMaterialStockApi.MaterialStock) { + if (!row.batchId) { + return; + } + batchDetailRef.value?.open(row.batchId); +} + /** 处理冻结状态切换 */ async function handleFrozenChange( + newFrozen: boolean, row: MesWmMaterialStockApi.MaterialStock, ): Promise { - const text = row.frozen ? '冻结' : '解冻'; + const text = newFrozen ? '冻结' : '解冻'; try { await confirm(`确认要"${text}"该库存记录吗?`); } catch { @@ -68,7 +80,7 @@ async function handleFrozenChange( // 更新冻结状态 await updateMaterialStockFrozen({ id: row.id!, - frozen: row.frozen!, + frozen: newFrozen, }); // 提示并返回成功 message.success(`${text}成功`); @@ -117,6 +129,7 @@ const [Grid, gridApi] = useVbenVxeGrid({ +
@@ -140,9 +153,15 @@ const [Grid, gridApi] = useVbenVxeGrid({ /> +
@@ -131,9 +146,16 @@ async function handleExport() { />