feat(mes): 迁移“生产报工(pro_feedback)”的 antd 功能

pull/349/head
YunaiV 2026-05-26 08:42:49 +08:00
parent 4a92762d44
commit 3acc821de5
14 changed files with 1613 additions and 0 deletions

View File

@ -0,0 +1,113 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace MesProFeedbackApi {
/** MES 生产报工 */
export interface Feedback {
id?: number;
code?: string; // 报工单编号
type?: number; // 报工类型
channel?: string; // 报工途径
feedbackTime?: number; // 报工时间
workstationId?: number; // 工作站编号
workstationCode?: string; // 工作站编码
workstationName?: string; // 工作站名称
routeId?: number; // 工艺路线编号
routeCode?: string; // 工艺路线编码
processId?: number; // 工序编号
processCode?: string; // 工序编码
processName?: string; // 工序名称
checkFlag?: boolean; // 是否需要检验
workOrderId?: number; // 生产工单编号
workOrderCode?: string; // 工单编码
workOrderName?: string; // 工单名称
taskId?: number; // 生产任务编号
taskCode?: string; // 任务编码
itemId?: number; // 产品物料编号
itemCode?: string; // 物料编码
itemName?: string; // 物料名称
itemSpecification?: string; // 规格型号
unitMeasureId?: number; // 单位编号
unitMeasureName?: string; // 单位名称
expireDate?: number; // 过期日期
scheduledQuantity?: number; // 排产数量
feedbackQuantity?: number; // 本次报工数量
qualifiedQuantity?: number; // 合格品数量
unqualifiedQuantity?: number; // 不良品数量
uncheckQuantity?: number; // 待检测数量
laborScrapQuantity?: number; // 工废数量
materialScrapQuantity?: number; // 料废数量
otherScrapQuantity?: number; // 其他废品数量
feedbackUserId?: number; // 报工用户编号
feedbackUserNickname?: string; // 报工人昵称
approveUserId?: number; // 审核用户编号
approveUserNickname?: string; // 审核人昵称
status?: number; // 状态
remark?: string; // 备注
creator?: string; // 创建人
createTime?: number; // 创建时间
}
/** MES 生产报工分页查询参数 */
export interface PageParams extends PageParam {
code?: string;
type?: number;
workOrderId?: number;
itemId?: number;
feedbackUserId?: number;
creator?: string;
status?: number;
feedbackTime?: string[];
}
}
/** 查询生产报工分页 */
export function getFeedbackPage(params: MesProFeedbackApi.PageParams) {
return requestClient.get<PageResult<MesProFeedbackApi.Feedback>>(
'/mes/pro/feedback/page',
{ params },
);
}
/** 查询生产报工详情 */
export function getFeedback(id: number) {
return requestClient.get<MesProFeedbackApi.Feedback>(
`/mes/pro/feedback/get?id=${id}`,
);
}
/** 新增生产报工 */
export function createFeedback(data: MesProFeedbackApi.Feedback) {
return requestClient.post<number>('/mes/pro/feedback/create', data);
}
/** 修改生产报工 */
export function updateFeedback(data: MesProFeedbackApi.Feedback) {
return requestClient.put('/mes/pro/feedback/update', data);
}
/** 删除生产报工 */
export function deleteFeedback(id: number) {
return requestClient.delete(`/mes/pro/feedback/delete?id=${id}`);
}
/** 导出生产报工 Excel */
export function exportFeedback(params: Partial<MesProFeedbackApi.PageParams>) {
return requestClient.download('/mes/pro/feedback/export-excel', { params });
}
/** 提交生产报工 */
export function submitFeedback(id: number) {
return requestClient.put(`/mes/pro/feedback/submit?id=${id}`);
}
/** 驳回生产报工 */
export function rejectFeedback(id: number) {
return requestClient.put(`/mes/pro/feedback/reject?id=${id}`);
}
/** 审批生产报工(返回是否已审批完成) */
export function approveFeedback(id: number) {
return requestClient.put<boolean>(`/mes/pro/feedback/approve?id=${id}`);
}

View File

@ -0,0 +1,68 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace MesProTaskApi {
/** MES 生产任务 */
export interface Task {
id?: number;
code?: string; // 任务编码
name?: string; // 任务名称
workOrderId?: number; // 生产工单编号
workOrderCode?: string; // 工单编码
workOrderName?: string; // 工单名称
workstationId?: number; // 工作站编号
workstationCode?: string; // 工作站编码
workstationName?: string; // 工作站名称
routeId?: number; // 工艺路线编号
processId?: number; // 工序编号
processName?: string; // 工序名称
itemId?: number; // 产品物料编号
itemCode?: string; // 产品编码
itemName?: string; // 产品名称
itemSpecification?: string; // 规格型号
unitMeasureId?: number; // 单位编号
unitMeasureName?: string; // 单位名称
quantity?: number; // 排产数量
producedQuantity?: number; // 已生产数量
qualifyQuantity?: number; // 合格品数量
unqualifyQuantity?: number; // 不良品数量
changedQuantity?: number; // 调整数量
clientId?: number; // 客户编号
clientName?: string; // 客户名称
startTime?: number; // 开始生产时间
endTime?: number; // 结束生产时间
duration?: number; // 生产时长工作日1=8小时
requestDate?: number; // 需求日期(从工单查)
finishDate?: number; // 完成日期
cancelDate?: number; // 取消日期
colorCode?: string; // 甘特图显示颜色
status?: number; // 任务状态
checkFlag?: boolean; // 是否质检(派生自工艺路线工序)
remark?: string; // 备注
}
/** MES 生产任务分页查询参数 */
export interface PageParams extends PageParam {
code?: string;
name?: string;
workOrderId?: number;
workstationId?: number;
itemId?: number;
statuses?: number[];
status?: number;
}
}
/** 查询生产任务分页 */
export function getTaskPage(params: MesProTaskApi.PageParams) {
return requestClient.get<PageResult<MesProTaskApi.Task>>(
'/mes/pro/task/page',
{ params },
);
}
/** 查询生产任务详情 */
export function getTask(id: number) {
return requestClient.get<MesProTaskApi.Task>(`/mes/pro/task/get?id=${id}`);
}

View File

@ -0,0 +1,37 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace MesWmItemConsumeLineApi {
/** MES 物料消耗行 */
export interface ItemConsumeLine {
id?: number;
feedbackId?: number; // 报工编号
itemId?: number; // 物料编号
itemCode?: string; // 物资编码
itemName?: string; // 物资名称
specification?: string; // 规格型号
unitId?: number; // 单位编号
unitName?: string; // 单位
quantity?: number; // 消耗数量
batchCode?: string; // 批次号
locationId?: number; // 库位编号
locationName?: string; // 库位名称
remark?: string; // 备注
}
/** MES 物料消耗行分页查询参数 */
export interface PageParams extends PageParam {
feedbackId?: number;
}
}
/** 查询物料消耗行分页 */
export function getItemConsumeLinePage(
params: MesWmItemConsumeLineApi.PageParams,
) {
return requestClient.get<PageResult<MesWmItemConsumeLineApi.ItemConsumeLine>>(
'/mes/wm/item-consume-line/page',
{ params },
);
}

View File

@ -0,0 +1,37 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace MesWmProductProduceLineApi {
/** MES 产品产出行 */
export interface ProductProduceLine {
id?: number;
feedbackId?: number; // 报工编号
itemId?: number; // 物料编号
itemCode?: string; // 物资编码
itemName?: string; // 物资名称
specification?: string; // 规格型号
unitMeasureId?: number; // 单位编号
unitMeasureName?: string; // 单位
quantity?: number; // 产出数量
batchCode?: string; // 批次号
qualityStatus?: number; // 质量状态
locationId?: number; // 库位编号
locationName?: string; // 库位名称
remark?: string; // 备注
}
/** MES 产品产出行分页查询参数 */
export interface PageParams extends PageParam {
feedbackId?: number;
}
}
/** 查询产品产出行分页 */
export function getProductProduceLinePage(
params: MesWmProductProduceLineApi.PageParams,
) {
return requestClient.get<
PageResult<MesWmProductProduceLineApi.ProductProduceLine>
>('/mes/wm/product-produce-line/page', { params });
}

View File

@ -0,0 +1,575 @@
import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProFeedbackApi } from '#/api/mes/pro/feedback';
import type { MesProTaskApi } from '#/api/mes/pro/task';
import { h, markRaw } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { Button } from 'ant-design-vue';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import { getRouteProcessByRouteAndProcess } from '#/api/mes/pro/route/process';
import { getSimpleUserList } from '#/api/system/user';
import { getRangePickerDefaultProps } from '#/utils';
import { MdItemSelect } from '#/views/mes/md/item/components';
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 {
MesAutoCodeRuleCode,
MesProTaskStatusEnum,
MesProWorkOrderStatusEnum,
} from '#/views/mes/utils/constants';
/** 列表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'code',
label: '报工单号',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入报工单号',
},
},
{
fieldName: 'type',
label: '报工类型',
component: 'Select',
componentProps: {
allowClear: true,
options: getDictOptions(DICT_TYPE.MES_PRO_FEEDBACK_TYPE, 'number'),
placeholder: '请选择报工类型',
},
},
{
fieldName: 'workOrderId',
label: '生产工单',
component: markRaw(ProWorkOrderSelect),
componentProps: {
allowClear: true,
placeholder: '请选择工单',
},
},
{
fieldName: 'itemId',
label: '产品物料',
component: markRaw(MdItemSelect),
componentProps: {
allowClear: true,
placeholder: '请选择产品物料',
},
},
{
fieldName: 'feedbackUserId',
label: '报工人',
component: 'ApiSelect',
componentProps: {
allowClear: true,
api: getSimpleUserList,
labelField: 'nickname',
placeholder: '请选择报工人',
valueField: 'id',
},
},
{
fieldName: 'creator',
label: '记录人',
component: 'ApiSelect',
componentProps: {
allowClear: true,
api: getSimpleUserList,
labelField: 'nickname',
placeholder: '请选择记录人',
valueField: 'id',
},
},
{
fieldName: 'status',
label: '状态',
component: 'Select',
componentProps: {
allowClear: true,
options: getDictOptions(DICT_TYPE.MES_PRO_FEEDBACK_STATUS, 'number'),
placeholder: '请选择状态',
},
},
{
fieldName: 'feedbackTime',
label: '报工时间',
component: 'RangePicker',
componentProps: {
...getRangePickerDefaultProps(),
allowClear: true,
},
},
];
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions<MesProFeedbackApi.Feedback>['columns'] {
return [
{
field: 'code',
title: '报工单号',
width: 160,
slots: { default: 'code' },
},
{
field: 'type',
title: '报工类型',
width: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.MES_PRO_FEEDBACK_TYPE },
},
},
{ field: 'workstationName', title: '工作站', width: 120 },
{ field: 'processName', title: '工序', width: 100 },
{ field: 'workOrderCode', title: '生产工单编码', width: 160 },
{ field: 'itemCode', title: '产品物料编码', width: 120 },
{ field: 'itemName', title: '产品物料名称', minWidth: 140 },
{ field: 'itemSpecification', title: '规格型号', width: 120 },
{ field: 'unitMeasureName', title: '单位', width: 80 },
{ field: 'feedbackQuantity', title: '报工数量', width: 100 },
{ field: 'feedbackUserNickname', title: '报工人', width: 100 },
{
field: 'feedbackTime',
title: '报工时间',
width: 180,
formatter: 'formatDateTime',
},
{ field: 'approveUserNickname', title: '审核人', width: 100 },
{
field: 'status',
title: '状态',
width: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.MES_PRO_FEEDBACK_STATUS },
},
},
{
title: '操作',
width: 240,
fixed: 'right',
slots: { default: 'actions' },
},
];
}
/**
* ////
*
* - create / update
* - submit / approve / detail
*
* `checkFlag` `unqualifiedQuantity`
* - = + > 0 //
* -
*/
export function useFormSchema(
formType: string,
formApi?: VbenFormApi,
): VbenFormSchema[] {
const isHeaderReadonly = ['approve', 'detail', 'submit'].includes(formType);
return [
{
fieldName: 'id',
component: 'Input',
dependencies: { triggerFields: [''], show: () => false },
},
{
fieldName: 'checkFlag',
component: 'Input',
dependencies: { triggerFields: [''], show: () => false },
defaultValue: true,
},
{
fieldName: 'routeId',
component: 'Input',
dependencies: { triggerFields: [''], show: () => false },
},
{
fieldName: 'processId',
component: 'Input',
dependencies: { triggerFields: [''], show: () => false },
},
{
fieldName: 'itemId',
component: 'Input',
dependencies: { triggerFields: [''], show: () => false },
},
{
fieldName: 'code',
label: '报工单号',
component: 'Input',
componentProps: {
disabled: isHeaderReadonly,
placeholder: '请输入报工单号',
},
rules: 'required',
suffix: () =>
h(
Button,
{
disabled: isHeaderReadonly,
type: 'default',
onClick: async () => {
const code = await generateAutoCode(
MesAutoCodeRuleCode.PRO_FEEDBACK_CODE,
);
await formApi?.setFieldValue('code', code);
},
},
{ default: () => '生成' },
),
},
{
fieldName: 'type',
label: '报工类型',
component: 'Select',
componentProps: {
disabled: isHeaderReadonly,
options: getDictOptions(DICT_TYPE.MES_PRO_FEEDBACK_TYPE, 'number'),
placeholder: '请选择报工类型',
},
rules: 'required',
},
{
fieldName: 'workOrderId',
label: '生产工单',
component: markRaw(ProWorkOrderSelect),
componentProps: {
disabled: isHeaderReadonly,
placeholder: '请选择工单',
status: MesProWorkOrderStatusEnum.CONFIRMED,
// 工单变更:清空任务及任务带出的产品信息、数量区域控制位
onChange: async () => {
await formApi?.setValues({
checkFlag: true,
itemCode: undefined,
itemId: undefined,
itemName: undefined,
itemSpecification: undefined,
processId: undefined,
routeId: undefined,
taskId: undefined,
unitMeasureName: undefined,
workstationId: undefined,
});
},
},
rules: 'selectRequired',
},
{
fieldName: 'taskId',
label: '生产任务',
component: markRaw(ProTaskSelect),
dependencies: {
triggerFields: ['workOrderId', 'workstationId'],
componentProps: (values) => ({
disabled: isHeaderReadonly || !values.workOrderId,
placeholder: values.workOrderId ? '请选择任务' : '请先选择工单',
statuses: [MesProTaskStatusEnum.PREPARE],
workOrderId: values.workOrderId,
workstationId: values.workstationId,
}),
},
// 任务变更自动填充关联字段、产品信息、checkFlag
componentProps: {
onChange: async (task?: MesProTaskApi.Task) => {
if (!task) {
return;
}
await formApi?.setValues({
itemCode: task.itemCode,
itemId: task.itemId,
itemName: task.itemName,
itemSpecification: task.itemSpecification,
processId: task.processId,
routeId: task.routeId,
unitMeasureName: task.unitMeasureName,
workstationId: task.workstationId,
});
// 工艺路线工序的 checkFlag 决定数量区域展示
if (task.routeId && task.processId) {
try {
const routeProcess = await getRouteProcessByRouteAndProcess(
task.routeId,
task.processId,
);
await formApi?.setFieldValue(
'checkFlag',
routeProcess?.checkFlag ?? false,
);
} catch {
await formApi?.setFieldValue('checkFlag', true);
}
}
},
},
rules: 'selectRequired',
},
{
fieldName: 'workstationId',
label: '工作站',
component: markRaw(MdWorkstationSelect),
componentProps: {
disabled: isHeaderReadonly,
placeholder: '请选择工作站',
},
rules: 'selectRequired',
},
{
fieldName: 'itemCode',
label: '产品编码',
component: 'Input',
componentProps: { disabled: true },
dependencies: {
triggerFields: ['itemCode'],
show: (values) => !!values.itemCode,
},
},
{
fieldName: 'itemName',
label: '产品名称',
component: 'Input',
componentProps: { disabled: true },
dependencies: {
triggerFields: ['itemCode'],
show: (values) => !!values.itemCode,
},
},
{
fieldName: 'unitMeasureName',
label: '单位',
component: 'Input',
componentProps: { disabled: true },
dependencies: {
triggerFields: ['itemCode'],
show: (values) => !!values.itemCode,
},
},
{
fieldName: 'itemSpecification',
label: '规格',
component: 'Input',
componentProps: { disabled: true },
dependencies: {
triggerFields: ['itemCode'],
show: (values) => !!values.itemCode,
},
},
{
fieldName: 'feedbackQuantity',
label: '报工数量',
component: 'InputNumber',
componentProps: { class: 'w-full', min: 0, precision: 2 },
dependencies: {
triggerFields: ['checkFlag'],
// 非质检工序时,报工数量 = 合格 + 不良,禁用直接编辑
componentProps: (values) => ({
class: 'w-full',
disabled: !values.checkFlag,
min: 0,
placeholder: '请输入报工数量',
precision: 2,
}),
},
rules: 'required',
},
{
fieldName: 'qualifiedQuantity',
label: '合格品数量',
component: 'InputNumber',
componentProps: {
class: 'w-full',
min: 0,
precision: 2,
// 合格/不良变更,自动累计为报工数量
onChange: async () => {
const values = await formApi?.getValues();
await formApi?.setFieldValue(
'feedbackQuantity',
(values?.qualifiedQuantity || 0) +
(values?.unqualifiedQuantity || 0),
);
},
},
defaultValue: 0,
dependencies: {
triggerFields: ['checkFlag'],
show: (values) => !values.checkFlag,
},
},
{
fieldName: 'unqualifiedQuantity',
label: '不良品数量',
component: 'InputNumber',
componentProps: {
class: 'w-full',
min: 0,
precision: 2,
// 合格/不良变更,自动累计为报工数量
onChange: async () => {
const values = await formApi?.getValues();
await formApi?.setFieldValue(
'feedbackQuantity',
(values?.qualifiedQuantity || 0) +
(values?.unqualifiedQuantity || 0),
);
},
},
defaultValue: 0,
dependencies: {
triggerFields: ['checkFlag'],
show: (values) => !values.checkFlag,
},
},
{
fieldName: 'laborScrapQuantity',
label: '工废数量',
component: 'InputNumber',
componentProps: {
class: 'w-full',
min: 0,
precision: 2,
// 废品分类变更,自动累计为不良品数量及报工数量
onChange: async () => {
const values = await formApi?.getValues();
const unqualified =
(values?.laborScrapQuantity || 0) +
(values?.materialScrapQuantity || 0) +
(values?.otherScrapQuantity || 0);
await formApi?.setValues({
feedbackQuantity:
(values?.qualifiedQuantity || 0) + unqualified,
unqualifiedQuantity: unqualified,
});
},
},
defaultValue: 0,
dependencies: {
triggerFields: ['checkFlag', 'unqualifiedQuantity'],
show: (values) =>
!values.checkFlag && (values.unqualifiedQuantity || 0) > 0,
},
},
{
fieldName: 'materialScrapQuantity',
label: '料废数量',
component: 'InputNumber',
componentProps: {
class: 'w-full',
min: 0,
precision: 2,
// 废品分类变更,自动累计为不良品数量及报工数量
onChange: async () => {
const values = await formApi?.getValues();
const unqualified =
(values?.laborScrapQuantity || 0) +
(values?.materialScrapQuantity || 0) +
(values?.otherScrapQuantity || 0);
await formApi?.setValues({
feedbackQuantity:
(values?.qualifiedQuantity || 0) + unqualified,
unqualifiedQuantity: unqualified,
});
},
},
defaultValue: 0,
dependencies: {
triggerFields: ['checkFlag', 'unqualifiedQuantity'],
show: (values) =>
!values.checkFlag && (values.unqualifiedQuantity || 0) > 0,
},
},
{
fieldName: 'otherScrapQuantity',
label: '其他废品',
component: 'InputNumber',
componentProps: {
class: 'w-full',
min: 0,
precision: 2,
// 废品分类变更,自动累计为不良品数量及报工数量
onChange: async () => {
const values = await formApi?.getValues();
const unqualified =
(values?.laborScrapQuantity || 0) +
(values?.materialScrapQuantity || 0) +
(values?.otherScrapQuantity || 0);
await formApi?.setValues({
feedbackQuantity:
(values?.qualifiedQuantity || 0) + unqualified,
unqualifiedQuantity: unqualified,
});
},
},
defaultValue: 0,
dependencies: {
triggerFields: ['checkFlag', 'unqualifiedQuantity'],
show: (values) =>
!values.checkFlag && (values.unqualifiedQuantity || 0) > 0,
},
},
{
fieldName: 'feedbackUserId',
label: '报工人',
component: 'ApiSelect',
componentProps: {
allowClear: true,
api: getSimpleUserList,
disabled: isHeaderReadonly,
labelField: 'nickname',
placeholder: '请选择报工人',
valueField: 'id',
},
rules: 'selectRequired',
},
{
fieldName: 'feedbackTime',
label: '报工时间',
component: 'DatePicker',
componentProps: {
class: 'w-full',
disabled: isHeaderReadonly,
format: 'YYYY-MM-DD HH:mm:ss',
placeholder: '请选择报工时间',
showTime: true,
valueFormat: 'x',
},
rules: 'required',
},
{
fieldName: 'approveUserId',
label: '审核人',
component: 'ApiSelect',
componentProps: {
allowClear: true,
api: getSimpleUserList,
disabled: isHeaderReadonly,
labelField: 'nickname',
placeholder: '请选择审核人',
valueField: 'id',
},
rules: 'selectRequired',
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
formItemClass: 'col-span-3',
componentProps: {
disabled: formType === 'detail',
placeholder: '请输入备注',
rows: 3,
},
},
];
}

View File

@ -0,0 +1,182 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProFeedbackApi } from '#/api/mes/pro/feedback';
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
import { useUserStore } from '@vben/stores';
import { downloadFileFromBlobPart } from '@vben/utils';
import { Button, message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
deleteFeedback,
exportFeedback,
getFeedbackPage,
} from '#/api/mes/pro/feedback';
import { $t } from '#/locales';
import { MesProFeedbackStatusEnum } from '#/views/mes/utils/constants';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
const userStore = useUserStore();
const currentUserId = userStore.userInfo?.id; // ID
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
destroyOnClose: true,
});
/** 刷新表格 */
function handleRefresh() {
gridApi.query();
}
/** 创建生产报工 */
function handleCreate() {
formModalApi.setData({ type: 'create' }).open();
}
/** 编辑生产报工 */
function handleEdit(row: MesProFeedbackApi.Feedback) {
formModalApi.setData({ id: row.id, type: 'update' }).open();
}
/** 提交生产报工 */
function handleSubmit(row: MesProFeedbackApi.Feedback) {
formModalApi.setData({ id: row.id, type: 'submit' }).open();
}
/** 审批生产报工 */
function handleApprove(row: MesProFeedbackApi.Feedback) {
formModalApi.setData({ id: row.id, type: 'approve' }).open();
}
/** 详情生产报工 */
function handleDetail(row: MesProFeedbackApi.Feedback) {
formModalApi.setData({ id: row.id, type: 'detail' }).open();
}
/** 删除生产报工 */
async function handleDelete(row: MesProFeedbackApi.Feedback) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.code]),
duration: 0,
});
try {
await deleteFeedback(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.code]));
handleRefresh();
} finally {
hideLoading();
}
}
/** 导出表格 */
async function handleExport() {
const data = await exportFeedback(await gridApi.formApi.getValues());
downloadFileFromBlobPart({ fileName: '生产报工.xls', source: data });
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: { schema: useGridFormSchema() },
gridOptions: {
columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getFeedbackPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: { isHover: true, keyField: 'id' },
toolbarConfig: { refresh: true, search: true },
} as VxeTableGridOptions<MesProFeedbackApi.Feedback>,
});
</script>
<template>
<Page auto-content-height>
<template #doc>
<DocAlert
title="【生产】生产报工"
url="https://doc.iocoder.cn/mes/pro/feedback/"
/>
</template>
<FormModal @success="handleRefresh" />
<Grid table-title="">
<template #toolbar-tools>
<TableAction
:actions="[
{
label: $t('ui.actionTitle.create', ['生产报工']),
type: 'primary',
icon: ACTION_ICON.ADD,
auth: ['mes:pro-feedback:create'],
onClick: handleCreate,
},
{
label: $t('ui.actionTitle.export'),
type: 'primary',
icon: ACTION_ICON.DOWNLOAD,
auth: ['mes:pro-feedback:export'],
onClick: handleExport,
},
]"
/>
</template>
<template #code="{ row }">
<Button type="link" @click="handleDetail(row)">
{{ row.code }}
</Button>
</template>
<template #actions="{ row }">
<TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
auth: ['mes:pro-feedback:update'],
ifShow: () => row.status === MesProFeedbackStatusEnum.PREPARE,
onClick: handleEdit.bind(null, row),
},
{
label: $t('common.submit'),
type: 'link',
auth: ['mes:pro-feedback:update'],
ifShow: () => row.status === MesProFeedbackStatusEnum.PREPARE,
onClick: handleSubmit.bind(null, row),
},
{
label: $t('common.approve'),
type: 'link',
auth: ['mes:pro-feedback:approve'],
ifShow: () =>
row.status === MesProFeedbackStatusEnum.APPROVING &&
row.approveUserId === currentUserId,
onClick: handleApprove.bind(null, row),
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
auth: ['mes:pro-feedback:delete'],
ifShow: () => row.status === MesProFeedbackStatusEnum.PREPARE,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.code]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template>
</Grid>
</Page>
</template>

View File

@ -0,0 +1,307 @@
<script lang="ts" setup>
import type { MesProFeedbackApi } from '#/api/mes/pro/feedback';
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { useUserStore } from '@vben/stores';
import { Button, message, Popconfirm, Tabs } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import {
approveFeedback,
createFeedback,
getFeedback,
rejectFeedback,
submitFeedback,
updateFeedback,
} from '#/api/mes/pro/feedback';
import { getRouteProcessByRouteAndProcess } from '#/api/mes/pro/route/process';
import { $t } from '#/locales';
import {
MesAutoCodeRuleCode,
MesProFeedbackStatusEnum,
} from '#/views/mes/utils/constants';
import { useFormSchema } from '../data';
import ItemConsumeList from './item-consume-list.vue';
import ProductProduceList from './product-produce-list.vue';
// TODO @AIformType
type FormMode = 'approve' | 'create' | 'detail' | 'submit' | 'update';
const emit = defineEmits(['success']);
const formMode = ref<FormMode>('create');
const formData = ref<MesProFeedbackApi.Feedback>();
const userStore = useUserStore();
const subTabsName = ref('itemConsume');
const isEditable = computed(() =>
['create', 'submit', 'update'].includes(formMode.value),
);
// TODO @AI isXXXX
const canSubmitDirectly = computed(
() =>
isEditable.value &&
formData.value?.status === MesProFeedbackStatusEnum.PREPARE,
);
const canApprove = computed(() => formMode.value === 'approve');
const showSubTabs = computed(
() =>
!!formData.value?.id &&
formData.value?.status !== MesProFeedbackStatusEnum.PREPARE &&
formData.value?.status !== MesProFeedbackStatusEnum.APPROVING,
);
const getTitle = computed(() => {
if (formMode.value === 'detail') {
return $t('ui.actionTitle.view', ['生产报工']);
}
if (formMode.value === 'approve') {
return '审批生产报工';
}
if (formMode.value === 'submit') {
return '提交生产报工';
}
return formMode.value === 'update'
? $t('ui.actionTitle.edit', ['生产报工'])
: $t('ui.actionTitle.create', ['生产报工']);
});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-1',
labelWidth: 110,
},
layout: 'horizontal',
schema: [],
showDefaultActions: false,
wrapperClass: 'grid-cols-3',
});
/** 表单 schema 需要 formApi 引用,所以通过 setState 设置 schema */
formApi.setState({ schema: useFormSchema(formMode.value, formApi) });
/** 提交前对齐数量:根据 checkFlag 决定 uncheck/合格/不良归零策略 */
function alignQuantity(data: MesProFeedbackApi.Feedback) {
if (data.checkFlag) {
data.uncheckQuantity = data.feedbackQuantity;
data.qualifiedQuantity = 0;
data.unqualifiedQuantity = 0;
data.laborScrapQuantity = 0;
data.materialScrapQuantity = 0;
data.otherScrapQuantity = 0;
} else {
data.feedbackQuantity =
(data.qualifiedQuantity || 0) + (data.unqualifiedQuantity || 0);
data.uncheckQuantity = 0;
}
}
/** 保存create 后切换为 update 模式 */
async function handleSave() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
try {
const data = (await formApi.getValues()) as MesProFeedbackApi.Feedback;
alignQuantity(data);
if (formMode.value === 'create') {
const id = await createFeedback(data);
formData.value = {
...data,
id,
status: MesProFeedbackStatusEnum.PREPARE,
};
formMode.value = 'update';
formApi.setState({ schema: useFormSchema(formMode.value, formApi) });
await formApi.setFieldValue('id', id);
message.success($t('common.createSuccess'));
} else {
await updateFeedback(data);
formData.value = { ...formData.value, ...data };
message.success($t('common.updateSuccess'));
}
emit('success');
} finally {
modalApi.unlock();
}
}
/** 提交:保存最新内容后调用提交接口 */
async function handleSubmit() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
try {
const data = (await formApi.getValues()) as MesProFeedbackApi.Feedback;
alignQuantity(data);
let id = formData.value?.id;
if (formMode.value === 'create' || !id) {
id = await createFeedback(data);
} else {
await updateFeedback(data);
}
await submitFeedback(id!);
await modalApi.close();
emit('success');
message.success('报工单已提交');
} finally {
modalApi.unlock();
}
}
/** 审批通过 */
async function handleApprove() {
if (!formData.value?.id) {
return;
}
modalApi.lock();
try {
const finished = await approveFeedback(formData.value.id);
await modalApi.close();
emit('success');
message.success(finished ? '报工单已审批完成' : '报工成功,请等待质量检验完成!');
} finally {
modalApi.unlock();
}
}
/** 审批不通过 */
async function handleReject() {
if (!formData.value?.id) {
return;
}
modalApi.lock();
try {
await rejectFeedback(formData.value.id);
await modalApi.close();
emit('success');
message.success('报工单已驳回');
} finally {
modalApi.unlock();
}
}
/** 加载工序的 checkFlag 用于回显数量区域 */
async function resolveCheckFlag(routeId?: number, processId?: number) {
if (!routeId || !processId) {
return true;
}
try {
const routeProcess = await getRouteProcessByRouteAndProcess(
routeId,
processId,
);
return routeProcess?.checkFlag ?? false;
} catch {
return true;
}
}
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
if (formMode.value === 'detail' || formMode.value === 'approve') {
await modalApi.close();
return;
}
await handleSave();
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = undefined;
subTabsName.value = 'itemConsume';
return;
}
//
const data = modalApi.getData<{ id?: number; type?: FormMode }>();
formMode.value = data?.type || 'create';
formApi.setState({ schema: useFormSchema(formMode.value, formApi) });
// /
formApi.setDisabled(
formMode.value === 'approve' || formMode.value === 'detail',
);
modalApi.setState({
showConfirmButton:
formMode.value !== 'detail' && formMode.value !== 'approve',
});
await formApi.resetForm();
if (!data?.id) {
//
const code = await generateAutoCode(
MesAutoCodeRuleCode.PRO_FEEDBACK_CODE,
);
await formApi.setValues({
code,
feedbackTime: Date.now(),
feedbackUserId: userStore.userInfo?.id,
});
return;
}
modalApi.lock();
try {
formData.value = await getFeedback(data.id);
const checkFlag = await resolveCheckFlag(
formData.value.routeId,
formData.value.processId,
);
// values
await formApi.setValues({ ...formData.value, checkFlag });
} finally {
modalApi.unlock();
}
},
});
</script>
<template>
<Modal :title="getTitle" class="w-3/5">
<Form class="mx-4" />
<Tabs
v-if="showSubTabs"
v-model:active-key="subTabsName"
type="card"
class="mx-4 mt-2"
>
<Tabs.TabPane key="itemConsume" tab="BOM 物资消耗">
<ItemConsumeList :feedback-id="formData!.id!" />
</Tabs.TabPane>
<Tabs.TabPane key="productProduce" tab="产品产出">
<ProductProduceList :feedback-id="formData!.id!" />
</Tabs.TabPane>
</Tabs>
<template #prepend-footer>
<div class="flex flex-auto items-center justify-end gap-2">
<Popconfirm
v-if="canSubmitDirectly"
title="确认提交该报工单?提交后将不能修改。"
@confirm="handleSubmit"
>
<Button type="primary">{{ $t('common.submit') }}</Button>
</Popconfirm>
<Popconfirm
v-if="canApprove"
title="确认审批通过该报工单?"
@confirm="handleApprove"
>
<Button type="primary">通过</Button>
</Popconfirm>
<Popconfirm
v-if="canApprove"
title="确认驳回该报工单?"
@confirm="handleReject"
>
<Button danger>不通过</Button>
</Popconfirm>
</div>
</template>
</Modal>
</template>

View File

@ -0,0 +1,57 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmItemConsumeLineApi } from '#/api/mes/wm/itemconsume/line';
import { watch } from 'vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getItemConsumeLinePage } from '#/api/mes/wm/itemconsume/line';
const props = defineProps<{
feedbackId: number;
}>();
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: [
{ field: 'itemCode', title: '物资编码', minWidth: 120 },
{ field: 'itemName', title: '物资名称', minWidth: 140 },
{ field: 'specification', title: '规格型号', minWidth: 120 },
{ field: 'quantity', title: '消耗数量', minWidth: 100 },
{ field: 'unitName', title: '单位', minWidth: 80 },
{ field: 'batchCode', title: '批次号', minWidth: 120 },
],
height: 320,
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }) => {
// TODO @AI
if (!props.feedbackId) {
return { list: [], total: 0 };
}
return await getItemConsumeLinePage({
feedbackId: props.feedbackId,
pageNo: page.currentPage,
pageSize: page.pageSize,
});
},
},
},
// TODO @AI
rowConfig: { isHover: true, keyField: 'id' },
toolbarConfig: { refresh: false, search: false },
} as VxeTableGridOptions<MesWmItemConsumeLineApi.ItemConsumeLine>,
});
watch(
() => props.feedbackId,
() => {
gridApi.query();
},
);
</script>
<template>
<Grid />
</template>

View File

@ -0,0 +1,68 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmProductProduceLineApi } from '#/api/mes/wm/productproduce/line';
import { watch } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getProductProduceLinePage } from '#/api/mes/wm/productproduce/line';
const props = defineProps<{
feedbackId: number;
}>();
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: [
{ field: 'itemCode', title: '物资编码', minWidth: 120 },
{ field: 'itemName', title: '物资名称', minWidth: 140 },
{ field: 'specification', title: '规格型号', minWidth: 120 },
{ field: 'quantity', title: '产出数量', minWidth: 100 },
{ field: 'unitMeasureName', title: '单位', minWidth: 80 },
{ field: 'batchCode', title: '批次号', minWidth: 120 },
{
field: 'qualityStatus',
title: '质量状态',
minWidth: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.MES_WM_QUALITY_STATUS },
},
},
],
height: 320,
keepSource: true,
proxyConfig: {
ajax: {
query: async ({ page }) => {
// TODO @AI
if (!props.feedbackId) {
return { list: [], total: 0 };
}
return await getProductProduceLinePage({
feedbackId: props.feedbackId,
pageNo: page.currentPage,
pageSize: page.pageSize,
});
},
},
},
// TODO @AI
rowConfig: { isHover: true, keyField: 'id' },
toolbarConfig: { refresh: false, search: false },
} as VxeTableGridOptions<MesWmProductProduceLineApi.ProductProduceLine>,
});
watch(
() => props.feedbackId,
() => {
gridApi.query();
},
);
</script>
<template>
<Grid />
</template>

View File

@ -0,0 +1 @@
export { default as ProTaskSelect } from './pro-task-select.vue';

View File

@ -0,0 +1,163 @@
<script lang="ts" setup>
import type { MesProTaskApi } from '#/api/mes/pro/task';
import { computed, onMounted, ref, watch } from 'vue';
import { Select, Tag, Tooltip } from 'ant-design-vue';
import { getTask, getTaskPage } from '#/api/mes/pro/task';
// TODO @AI
/**
* MES 生产任务选择器轻量版
*
* 当前用于生产报工等只需要单选任务 ID 的业务页面
* - 默认按 `workOrderId` / `workstationId` / `statuses` 过滤拉取首页 100 条任务作为下拉
* - 编辑回显走 `getTask(id)`
* - 后续 `mes/pro/task` 完整迁移后可替换为带弹窗的复杂选择器
*/
defineOptions({ name: 'ProTaskSelect', inheritAttrs: false });
const props = withDefaults(
defineProps<{
allowClear?: boolean;
disabled?: boolean;
modelValue?: number;
pageSize?: number;
placeholder?: string;
statuses?: number[];
workOrderId?: number;
workstationId?: number;
}>(),
{
allowClear: true,
disabled: false,
modelValue: undefined,
pageSize: 100,
placeholder: '请选择任务',
statuses: undefined,
workOrderId: undefined,
workstationId: undefined,
},
);
const emit = defineEmits<{
change: [item: MesProTaskApi.Task | undefined];
'update:modelValue': [value: number | undefined];
}>();
const allList = ref<MesProTaskApi.Task[]>([]);
const selectedItem = ref<MesProTaskApi.Task>();
const selectValue = computed({
get: () => props.modelValue,
set: (value: number | undefined) => {
emit('update:modelValue', value);
},
});
/** 前端过滤:按任务编号或名称模糊匹配 */
function handleFilter(input: string, option: any) {
const keyword = input.toLowerCase();
const item = option?.item as MesProTaskApi.Task | undefined;
return Boolean(
item?.code?.toLowerCase().includes(keyword) ||
item?.name?.toLowerCase().includes(keyword),
);
}
/** 同步选中任务详情,未在列表内时单独拉取 */
async function syncSelectedItem(value: number | undefined) {
if (value === undefined) {
selectedItem.value = undefined;
return;
}
const found = allList.value.find((item) => item.id === value);
if (found) {
selectedItem.value = found;
return;
}
try {
selectedItem.value = await getTask(value);
} catch (error) {
console.error('[ProTaskSelect] resolveItemById failed:', error);
}
}
/** 除 v-model 外,额外抛出完整任务对象给业务表单使用 */
function handleChange(value: any) {
const nextValue = value === undefined ? undefined : Number(value);
syncSelectedItem(nextValue);
emit('change', selectedItem.value);
}
/** 重新拉取候选任务列表 */
async function loadList() {
const data = await getTaskPage({
pageNo: 1,
pageSize: props.pageSize,
statuses: props.statuses,
workOrderId: props.workOrderId,
workstationId: props.workstationId,
});
allList.value = data.list ?? [];
}
watch(
() => props.modelValue,
(value) => {
syncSelectedItem(value);
},
);
watch(
() => [props.workOrderId, props.workstationId],
async () => {
await loadList();
syncSelectedItem(props.modelValue);
},
);
onMounted(async () => {
await loadList();
syncSelectedItem(props.modelValue);
});
</script>
<template>
<Tooltip :mouse-enter-delay="0.5" :open="selectedItem ? undefined : false">
<template #title>
<div v-if="selectedItem" class="leading-6">
<div>任务编号{{ selectedItem.code || '-' }}</div>
<div>任务名称{{ selectedItem.name || '-' }}</div>
<div>工序{{ selectedItem.processName || '-' }}</div>
<div>工作站{{ selectedItem.workstationName || '-' }}</div>
<div>物料{{ selectedItem.itemName || '-' }}</div>
<div>规格{{ selectedItem.itemSpecification || '-' }}</div>
</div>
</template>
<Select
v-bind="$attrs"
v-model:value="selectValue"
:allow-clear="allowClear"
:disabled="disabled"
:filter-option="handleFilter"
:placeholder="placeholder"
class="w-full"
show-search
@change="handleChange"
>
<Select.Option
v-for="item in allList"
:key="item.id"
:item="item"
:value="item.id"
>
<div class="flex items-center gap-2">
<span>{{ item.code }}</span>
<Tag v-if="item.itemName" color="default">{{ item.itemName }}</Tag>
</div>
</Select.Option>
</Select>
</Tooltip>
</template>

View File

@ -216,6 +216,7 @@ const MES_DICT = {
MES_WM_BARCODE_BIZ_TYPE: 'mes_wm_barcode_biz_type', // MES 条码业务类型
MES_WM_BARCODE_FORMAT: 'mes_wm_barcode_format', // MES 条码格式
MES_WM_PRODUCT_SALES_STATUS: 'mes_wm_product_sales_status', // MES 销售出库单状态
MES_WM_QUALITY_STATUS: 'mes_wm_quality_status', // MES 质量状态
} as const;
/** ========== WMS - 仓储管理模块 ========== */

View File

@ -21,6 +21,8 @@
"detail": "Detail",
"yes": "Yes",
"no": "No",
"submit": "Submit",
"approve": "Approve",
"showSearchPanel": "Show search panel",
"hideSearchPanel": "Hide search panel"
}

View File

@ -21,6 +21,8 @@
"detail": "详情",
"yes": "是",
"no": "否",
"submit": "提交",
"approve": "审批",
"showSearchPanel": "显示搜索面板",
"hideSearchPanel": "隐藏搜索面板"
}