gaoluyang
6 天以前 2e8c5d342a60ae707f42350371c718c212f703f1
refactor(mes,wls): 生产计划审核改在协同办公,精简报工/工单归档入口

- 生产计划页去掉页面内审核弹窗与接口,新增审批详情页(detail/approve),审批在协同办公完成
- 生产报工、工单列表去掉「归档」入口(方案未要求)
- 工单操作列加宽到 440,避免多个链接按钮被裁切
- 生领料单弹窗改用 Modal onConfirm 提交,修复点「生成领料单」无反应
- 销售通知明细去掉「是否检验」列

Co-Authored-By: Claude Code <noreply@anthropic.com>
已添加1个文件
已修改8个文件
296 ■■■■■ 文件已修改
src/api/mes/pro/mps/index.ts 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/feedback/index.vue 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/mps/detail/approve/index.vue 112 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/mps/index.vue 101 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/workorder/data.ts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/workorder/index.vue 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/productissue/modules/generate.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/salesnotice/components/line-select-dialog.vue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/salesnotice/data.ts 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/mps/index.ts
@@ -76,12 +76,6 @@
    remark?: string;
  }
  /** å®¡æ ¸è¯·æ±‚ */
  export interface AuditReq {
    id: number;
    approved: boolean;
    auditRemark?: string;
  }
}
/** æŸ¥è¯¢ MPS åˆ†é¡µ */
@@ -136,11 +130,6 @@
/** æäº¤å®¡æ ¸ */
export function submitMps(id: number) {
  return requestClient.post(`/mes/pro-mps/submit?id=${id}`);
}
/** å®¡æ ¸ï¼ˆé€šè¿‡/驳回) */
export function auditMps(data: MesProMpsApi.AuditReq) {
  return requestClient.post('/mes/pro-mps/audit', data);
}
/** æŸ¥è¯¢è®¡åˆ’的辅料需求明细 */
src/views/mes/pro/feedback/index.vue
@@ -15,15 +15,12 @@
  exportFeedback,
  getFeedbackPage,
} from '#/api/mes/pro/feedback';
import { getWorkOrder } from '#/api/mes/pro/workorder';
import { $t } from '#/locales';
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
import ArchiveDetail from '#/views/mes/pd/archive/modules/detail.vue';
const userStore = useUserStore();
const currentUserId = userStore.userInfo?.id; // å½“前登录用户 ID,用于审批人权限判断
@@ -33,8 +30,6 @@
  connectedComponent: Form,
  destroyOnClose: true,
});
const archiveDetailRef = ref<InstanceType<typeof ArchiveDetail>>();
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
@@ -94,17 +89,6 @@
async function handleExport() {
  const data = await exportFeedback(await gridApi.formApi.getValues());
  downloadFileFromBlobPart({ fileName: '生产报工.xls', source: data });
}
/** æŸ¥çœ‹å½’æ¡£ */
async function handleArchive(row: MesProFeedbackApi.Feedback) {
  if (!row.workOrderId) return;
  try {
    const workOrder = await getWorkOrder(row.workOrderId);
    archiveDetailRef.value?.open(workOrder.productCode);
  } catch {
    message.error('获取工单信息失败');
  }
}
const [Grid, gridApi] = useVbenVxeGrid({
@@ -203,15 +187,9 @@
                confirm: handleDelete.bind(null, row),
              },
            },
            {
              label: '归档',
              type: 'link',
              onClick: handleArchive.bind(null, row),
            },
          ]"
        />
      </template>
    </Grid>
    <ArchiveDetail ref="archiveDetailRef" />
  </Page>
</template>
src/views/mes/pro/mps/detail/approve/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,112 @@
<script lang="ts" setup>
import type { MesProMpsApi } from '#/api/mes/pro/mps';
import type { DescriptionItemSchema } from '#/components/description';
import { h, onMounted, ref } from 'vue';
import { ContentWrap } from '@vben/common-ui';
import { formatDate } from '@vben/utils';
import { Spin, Tag } from 'ant-design-vue';
import { getMps } from '#/api/mes/pro/mps';
import { useDescription } from '#/components/description';
defineOptions({ name: 'MesProMpsApproveDetail' });
const props = defineProps<{ id: string }>();
const loading = ref(false);
const formData = ref<MesProMpsApi.Mps>();
/** æ–‡æœ¬å­—段空值占位 */
function textRender(val: unknown) {
  return val === 0 || val ? String(val) : '-';
}
/** çŠ¶æ€æ¸²æŸ“ */
function renderStatus(val?: number) {
  const map: Record<number, { text: string; color: string }> = {
    0: { text: '未开始', color: 'blue' },
    1: { text: '已下发', color: 'success' },
    2: { text: '已完成', color: 'purple' },
    3: { text: '已取消', color: 'red' },
    4: { text: '审核中', color: 'warning' },
    5: { text: '已审核', color: 'cyan' },
  };
  const item = map[val ?? 0]!;
  return h(Tag, { color: item.color }, () => item.text);
}
/** æ•°é‡æ¸²æŸ“:去掉多余小数位 */
function quantityRender(val?: number) {
  if (val === null || val === undefined) {
    return '-';
  }
  return Number(val).toString();
}
const [Descriptions] = useDescription({
  bordered: true,
  column: 2,
  schema: [
    { label: '计划编号', field: 'code', render: textRender },
    { label: '状态', field: 'status', render: (val) => renderStatus(val as number) },
    { label: '销售订单号', field: 'saleOrderNo', render: textRender },
    { label: '客户名称', field: 'clientName', render: textRender },
    { label: '产品编码', field: 'productCode', render: textRender },
    { label: '产品名称', field: 'productName', render: textRender },
    { label: '规格型号', field: 'productSpecification', render: textRender },
    { label: '单位', field: 'unitMeasureName', render: textRender },
    {
      label: '计划生产数量',
      field: 'quantity',
      render: (val) => quantityRender(val as number),
    },
    {
      label: '已生产数量',
      field: 'quantityProduced',
      render: (val) => quantityRender(val as number),
    },
    {
      label: '计划开始日期',
      field: 'planStartDate',
      render: (val) => (val ? (formatDate(val) as string) : '-'),
    },
    {
      label: '需求日期',
      field: 'requestDate',
      render: (val) => (val ? (formatDate(val) as string) : '-'),
    },
    {
      label: '创建时间',
      field: 'createTime',
      render: (val) => (val ? (formatDate(val) as string) : '-'),
    },
    { label: '备注', field: 'remark', render: textRender },
  ] as DescriptionItemSchema[],
});
onMounted(async () => {
  try {
    loading.value = true;
    formData.value = await getMps(Number(props.id));
  } finally {
    loading.value = false;
  }
});
</script>
<template>
  <ContentWrap class="m-2">
    <Spin :spinning="loading" tip="加载中...">
      <div class="rounded-lg border border-border bg-background p-4">
        <div class="mb-4 flex items-center gap-2">
          <span class="inline-block h-4 w-1 rounded bg-primary" />
          <span class="text-base font-semibold">生产计划信息</span>
        </div>
        <Descriptions :data="formData" />
      </div>
    </Spin>
  </ContentWrap>
</template>
src/views/mes/pro/mps/index.vue
@@ -8,19 +8,10 @@
import { Page, useVbenModal } from '@vben/common-ui';
import { downloadFileFromBlobPart, isEmpty } from '@vben/utils';
import {
  Button,
  message,
  Modal,
  Radio,
  RadioGroup,
  Tag,
  Textarea,
} from 'ant-design-vue';
import { Button, message, Modal, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  auditMps,
  cancelMps,
  deleteMps,
  exportMps,
@@ -53,15 +44,6 @@
const itemListOpen = ref(false);
const currentMpsId = ref<number>();
/** å®¡æ ¸å¼¹çª— */
const auditOpen = ref(false);
const auditSubmitting = ref(false);
const auditForm = ref<{ approved: boolean; remark: string }>({
  approved: true,
  remark: '',
});
const currentAuditRow = ref<MesProMpsApi.Mps>();
/** æ‰“开辅料明细 */
function handleShowItems(row: MesProMpsApi.Mps) {
  currentMpsId.value = row.id;
@@ -77,37 +59,6 @@
    handleRefresh();
  } finally {
    hideLoading();
  }
}
/** æ‰“开审核弹窗 */
function handleAudit(row: MesProMpsApi.Mps) {
  currentAuditRow.value = row;
  auditForm.value = { approved: true, remark: '' };
  auditOpen.value = true;
}
/** ç¡®è®¤å®¡æ ¸ */
async function handleAuditSubmit() {
  if (!currentAuditRow.value?.id) {
    return;
  }
  if (!auditForm.value.approved && !auditForm.value.remark.trim()) {
    message.warning('驳回时必须填写驳回原因');
    return;
  }
  auditSubmitting.value = true;
  try {
    await auditMps({
      id: currentAuditRow.value.id,
      approved: auditForm.value.approved,
      auditRemark: auditForm.value.remark.trim() || undefined,
    });
    message.success(auditForm.value.approved ? '审核通过' : '已驳回');
    auditOpen.value = false;
    handleRefresh();
  } finally {
    auditSubmitting.value = false;
  }
}
@@ -383,9 +334,8 @@
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['mes:pro-mps:update'],
              ifShow: () =>
                row.status === MpsStatusEnum.DRAFT ||
                row.status === MpsStatusEnum.AUDITING,
              // ä»…草稿可编辑;审核中的计划需在协同办公完成审批
              ifShow: () => row.status === MpsStatusEnum.DRAFT,
              onClick: handleEdit.bind(null, row),
            },
            {
@@ -393,21 +343,12 @@
              type: 'link',
              icon: ACTION_ICON.AUDIT,
              auth: ['mes:pro-mps:submit'],
              ifShow: () =>
                row.status === MpsStatusEnum.DRAFT ||
                row.status === MpsStatusEnum.AUDITING,
              // æäº¤åŽè¿›å…¥ååŒåŠžå…¬å®¡æ‰¹ï¼Œå®¡æ ¸ä¸­ä¸å†é‡å¤æäº¤
              ifShow: () => row.status === MpsStatusEnum.DRAFT,
              popConfirm: {
                title: `确认提交${row.code}审核吗?`,
                title: `确认提交${row.code}审核吗?提交后请到「协同办公」完成审批`,
                confirm: handleSubmit.bind(null, row),
              },
            },
            {
              label: '审核',
              type: 'link',
              icon: ACTION_ICON.AUDIT,
              auth: ['mes:pro-mps:audit'],
              ifShow: () => row.status === MpsStatusEnum.AUDITING,
              onClick: handleAudit.bind(null, row),
            },
            {
              label: '生成采购单',
@@ -446,9 +387,7 @@
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['mes:pro-mps:delete'],
              ifShow: () =>
                row.status === MpsStatusEnum.DRAFT ||
                row.status === MpsStatusEnum.AUDITING,
              ifShow: () => row.status === MpsStatusEnum.DRAFT,
              popConfirm: {
                title: $t('ui.actionMessage.deleteConfirm', [row.code]),
                confirm: handleDelete.bind(null, row),
@@ -467,32 +406,6 @@
      destroy-on-close
    >
      <ItemList v-if="itemListOpen && currentMpsId" :mps-id="currentMpsId" />
    </Modal>
    <Modal
      v-model:open="auditOpen"
      title="审核生产计划"
      :confirm-loading="auditSubmitting"
      ok-text="提交"
      cancel-text="取消"
      width="520px"
      @ok="handleAuditSubmit"
    >
      <div class="mb-3">{{ currentAuditRow?.code }} Â· {{ currentAuditRow?.productName }}</div>
      <div class="mb-2 flex items-center gap-4">
        <span>审核结果:</span>
        <RadioGroup v-model:value="auditForm.approved">
          <Radio :value="true">通过</Radio>
          <Radio :value="false">驳回</Radio>
        </RadioGroup>
      </div>
      <Textarea
        v-model:value="auditForm.remark"
        :rows="3"
        :maxlength="500"
        :placeholder="auditForm.approved ? '审核意见(可选)' : '请填写驳回原因(必填)'"
        show-count
      />
    </Modal>
  </Page>
</template>
src/views/mes/pro/workorder/data.ts
@@ -575,7 +575,9 @@
    },
    {
      title: '操作',
      width: 240,
      // å·²ç¡®è®¤è¡Œæœ€å¤šåŒæ—¶å‡ºçް 7 ä¸ªé“¾æŽ¥æŒ‰é’®ï¼ˆæ–°å¢žå­è®¢å•/批次管理/执行情况/打印领料单/启动生产/完成/取消),
      // æŒ‰ 14px å­—号实测总宽约 420,加单元格左右 6px å†…边距留到 440,避免被裁切
      width: 440,
      fixed: 'right',
      slots: { default: 'actions' },
    },
src/views/mes/pro/workorder/index.vue
@@ -22,7 +22,6 @@
    startWorkOrder,
  } from "#/api/mes/pro/workorder";
  import { $t } from "#/locales";
  import ArchiveDetail from '#/views/mes/pd/archive/modules/detail.vue';
// import { BarcodeDetail } from '#/views/wls/barcode/components';
  import { useGridColumns, useGridFormSchema } from "./data";
@@ -46,8 +45,6 @@
    destroyOnClose: true,
  });
  const archiveVisible = ref(false);
  const archiveProductCode = ref('');
// const barcodeDetailRef = ref<InstanceType<typeof BarcodeDetail>>(); // æ¡ç è¯¦æƒ…弹窗
  /** åˆ·æ–°è¡¨æ ¼ */
@@ -151,12 +148,6 @@
    await cancelWorkOrder(row.id!);
    message.success("订单已取消");
    handleRefresh();
  }
  /** æ‰“开归档详情 */
  function handleArchive(row: MesProWorkOrderApi.WorkOrder) {
    archiveProductCode.value = row.productCode || '';
    archiveVisible.value = true;
  }
  /** æŸ¥çœ‹è®¢å•条码 */
@@ -371,16 +362,10 @@
            //   auth: ['mes:pro-work-order:query'],
            //   onClick: handleBarcode.bind(null, row),
            // },
            {
              label: '归档',
              type: 'link',
              onClick: handleArchive.bind(null, row),
            },
          ]" />
      </template>
    </Grid>
    <!-- <BarcodeDetail ref="barcodeDetailRef" /> -->
    <ArchiveDetail :visible="archiveVisible" :product-code="archiveProductCode" @update:visible="archiveVisible = $event" />
  </Page>
</template>
src/views/wls/productissue/modules/generate.vue
@@ -116,6 +116,9 @@
}
const [Modal, modalApi] = useVbenModal({
  // å¼¹çª—底部「生成领料单」按钮点击回调:Vben Modal çš„确定按钮只会调用 modalApi.onConfirm,
  // æ¨¡æ¿ä¸Šå†™ @confirm ä¸ä¼šè§¦å‘(组件没有暴露 confirm äº‹ä»¶ï¼‰ï¼Œå¿…须在这里注册
  onConfirm: handleGenerate,
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      workOrderId.value = undefined;
@@ -141,7 +144,6 @@
    :confirm-text="'生成领料单'"
    :confirm-disabled="!workOrderId || !hasGap || loading"
    :confirm-loading="submitting"
    @confirm="handleGenerate"
  >
    <div class="mx-4 flex flex-col gap-3">
      <div class="flex items-center gap-2">
src/views/wls/salesnotice/components/line-select-dialog.vue
@@ -4,8 +4,6 @@
import { nextTick, ref } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { message, Modal } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
@@ -57,15 +55,6 @@
      field: 'batchCode',
      title: '批次号',
      minWidth: 120,
    },
    {
      field: 'oqcCheckFlag',
      title: '是否检验',
      width: 90,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
      },
    },
  ];
}
src/views/wls/salesnotice/data.ts
@@ -12,7 +12,6 @@
import { Button } from 'ant-design-vue';
import { z } from '#/adapter/form';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import CrmCustomerSelect from '#/components/crm-customer-select.vue';
import { ErpSaleOrderSelect } from '#/views/erp/sale/order/components';
@@ -323,15 +322,6 @@
      minWidth: 120,
    },
    {
      field: 'oqcCheckFlag',
      title: '是否检验',
      width: 90,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
      },
    },
    {
      field: 'remark',
      title: '备注',
      minWidth: 120,
@@ -380,12 +370,6 @@
      componentProps: {
        placeholder: '请输入批次号',
      },
    },
    {
      fieldName: 'oqcCheckFlag',
      label: '是否检验',
      component: 'Switch',
      rules: z.boolean().default(true),
    },
    {
      fieldName: 'remark',