2026-08-13 7dfbaba5930676252d8339f7d04861c1c2351d5b
feat(mes): 增加产品设计项目审批流程和批次追溯功能

- 添加审批中状态及流程选择弹窗组件
- 实现产品设计项目提交审批和查看审批详情功能
- 集成BPM审批流程接口和流程实例查询
- 新增批次采购来源、销售去向和工序流转追溯功能
- 添加SN码反查批次和多维度追溯展示
- 配置宁夏英泽防锈新材料公司信息
- 更新API接口地址配置
已修改11个文件
已添加4个文件
已删除1个文件
756 ■■■■■ 文件已修改
.env.production 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
company.config.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
dist.zip 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pd/project/index.ts 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/wm/batch/index.ts 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/constants/src/biz-mes-enum.ts 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/project/detail/index.vue 58 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/project/index.vue 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/project/modules/process-select-modal.vue 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/project/modules/workflow-timeline.vue 23 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/batchtrace/data.ts 175 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/batchtrace/index.vue 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/batchtrace/modules/process-trace-list.vue 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/batchtrace/modules/purchase-source-list.vue 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/batchtrace/modules/sales-target-list.vue 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/batchtrace/modules/trace-detail.vue 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.env.production
@@ -3,9 +3,9 @@
VITE_BASE=/
# è¯·æ±‚路径
VITE_BASE_URL=http://42.63.70.90:9003
VITE_BASE_URL=http://42.63.70.231:9001
# æŽ¥å£åœ°å€
VITE_GLOB_API_URL=http://42.63.70.90:9003/admin-api
VITE_GLOB_API_URL=http://42.63.70.231:9001/admin-api
# æ–‡ä»¶ä¸Šä¼ ç±»åž‹ï¼šserver - åŽç«¯ä¸Šä¼ ï¼Œ client - å‰ç«¯ç›´è¿žä¸Šä¼ ï¼Œä»…支持S3服务
VITE_UPLOAD_TYPE=server
company.config.ts
@@ -23,4 +23,10 @@
    favicon: '/logos/TEST.ico',
    apiUrl: 'http://42.63.70.90:9003/admin-api',
  },
  YZFX: {
    name: '宁夏英泽防锈新材料有限公司',
    logo: '/logos/TEST.png',
    favicon: '/logos/TEST.ico',
    apiUrl: 'http://42.63.70.231:9001/admin-api',
  },
};
dist.zip
Binary files differ
src/api/mes/pd/project/index.ts
@@ -32,6 +32,8 @@
    remark?: string;
    /** åˆ›å»ºæ—¶é—´ */
    createTime?: number;
    /** BPM æµç¨‹å®žä¾‹ç¼–号 */
    processInstanceId?: string;
  }
  export interface PageParams extends PageParam {
@@ -87,13 +89,20 @@
  });
}
/** å®¡æ ¸ï¼ˆå¾…审核→待归档) */
export function auditPdProject(id: number) {
  return requestClient.put('/mes/pd/project/audit', undefined, {
    params: { id },
/** æäº¤å®¡æ‰¹ï¼ˆå¾…审核→审批中) */
export function submitPdProjectApproval(id: number, processDefinitionKey: string) {
  return requestClient.put('/mes/pd/project/submit-approval', undefined, {
    params: { id, processDefinitionKey },
  });
}
/** èŽ·å¾—å®¡æ‰¹æµç¨‹åˆ—è¡¨ */
export function getPdProjectApproveProcessList() {
  return requestClient.get<Array<{ id: string; name: string; key: string }>>(
    '/mes/pd/project/approve-process-list',
  );
}
/** å½’档(待归档→已归档),后端自动创建归档台账记录 */
export function archivePdProject(id: number) {
  return requestClient.put('/mes/pd/project/archive', undefined, {
src/api/mes/wm/batch/index.ts
@@ -56,6 +56,56 @@
    expireDate?: string[]; // è¿‡æœŸæ—¥æœŸ
    receiptDate?: string[]; // å…¥åº“日期
  }
  /** MES æ‰¹æ¬¡é‡‡è´­æ¥æºï¼ˆé‡‡è´­å…¥åº“单 + IQC) */
  export interface PurchaseSource {
    receiptCode?: string; // é‡‡è´­å…¥åº“单号
    receiptDate?: Date; // å…¥åº“日期
    purchaseOrderCode?: string; // é‡‡è´­è®¢å•编号
    vendorId?: number; // ä¾›åº”商 ID
    vendorCode?: string; // ä¾›åº”商编码
    vendorName?: string; // ä¾›åº”商名称
    iqcCode?: string; // IQC æ£€éªŒå•号
    checkResult?: number; // IQC æ£€éªŒç»“论(字典 mes_qc_check_result)
    itemId?: number; // ç‰©æ–™ ID
    itemCode?: string; // ç‰©æ–™ç¼–码
    itemName?: string; // ç‰©æ–™åç§°
    batchCode?: string; // æ‰¹æ¬¡å·
    receivedQuantity?: number; // å…¥åº“数量
  }
  /** MES æ‰¹æ¬¡é”€å”®åŽ»å‘ï¼ˆé”€å”®å‡ºåº“å• + OQC) */
  export interface SalesTarget {
    salesCode?: string; // é”€å”®å‡ºåº“单号
    salesDate?: Date; // å‡ºåº“日期
    salesOrderCode?: string; // é”€å”®è®¢å•编号
    clientId?: number; // å®¢æˆ· ID
    clientCode?: string; // å®¢æˆ·ç¼–码
    clientName?: string; // å®¢æˆ·åç§°
    oqcCode?: string; // OQC æ£€éªŒå•号
    checkResult?: number; // OQC æ£€éªŒç»“论(字典 mes_qc_check_result)
    itemId?: number; // ç‰©æ–™ ID
    itemCode?: string; // ç‰©æ–™ç¼–码
    itemName?: string; // ç‰©æ–™åç§°
    batchCode?: string; // æ‰¹æ¬¡å·
    quantity?: number; // å‡ºåº“数量
    qualityStatus?: number; // è´¨é‡çŠ¶æ€ï¼ˆå­—å…¸ mes_wm_quality_status)
  }
  /** MES æ‰¹æ¬¡å·¥åºæµè½¬ï¼ˆäº§å‡º/投入事件) */
  export interface ProcessTrace {
    eventType?: 'OUT' | 'IN'; // äº‹ä»¶ç±»åž‹ï¼šOUT-产出 / IN-投入
    workOrderId?: number; // ç”Ÿäº§å·¥å• ID
    workOrderCode?: string; // ç”Ÿäº§å·¥å•编号
    processId?: number; // å·¥åº ID
    processCode?: string; // å·¥åºç¼–码
    processName?: string; // å·¥åºåç§°
    batchCode?: string; // æ‰¹æ¬¡å·
    quantity?: number; // æ•°é‡
    qualityStatus?: number; // è´¨é‡çŠ¶æ€ï¼ˆå­—å…¸ mes_wm_quality_status),仅产出侧有值
    eventTime?: Date; // äº‹ä»¶æ—¶é—´
    processSort?: number; // å·¥åºåºå·
  }
}
/** æŸ¥è¯¢æ‰¹æ¬¡è¯¦æƒ… */
@@ -88,3 +138,34 @@
    { params: { code } },
  );
}
/** æ‰¹æ¬¡é‡‡è´­æ¥æºï¼ˆé‡‡è´­å…¥åº“单 + IQC) */
export function getPurchaseSourceList(code: string) {
  return requestClient.get<MesWmBatchApi.PurchaseSource[]>(
    '/mes/wm/batch/purchase-sources',
    { params: { code } },
  );
}
/** æ‰¹æ¬¡é”€å”®åŽ»å‘ï¼ˆé”€å”®å‡ºåº“å• + OQC) */
export function getSalesTargetList(code: string) {
  return requestClient.get<MesWmBatchApi.SalesTarget[]>(
    '/mes/wm/batch/sales-targets',
    { params: { code } },
  );
}
/** æ‰¹æ¬¡å·¥åºæµè½¬ï¼ˆäº§å‡º/投入事件) */
export function getProcessTraceList(code: string) {
  return requestClient.get<MesWmBatchApi.ProcessTrace[]>(
    '/mes/wm/batch/process-trace',
    { params: { code } },
  );
}
/** æ ¹æ® SN ç åæŸ¥æ‰¹æ¬¡ */
export function getBatchBySn(sn: string) {
  return requestClient.get<MesWmBatchApi.Batch | null>('/mes/wm/batch/get-by-sn', {
    params: { sn },
  });
}
src/packages/constants/src/biz-mes-enum.ts
@@ -530,11 +530,12 @@
  RIGHT: 2, // å³è¡¥é½
} as const;
/** MES äº§å“è®¾è®¡é¡¹ç›®çŠ¶æ€æžšä¸¾ï¼ˆçº¿æ€§æµè½¬ï¼šå¾…å‘å¸ƒâ†’å¾…ä¸Šä¼ â†’å¾…å®¡æ ¸â†’å¾…å½’æ¡£â†’å·²å½’æ¡£ï¼‰ */
/** MES äº§å“è®¾è®¡é¡¹ç›®çŠ¶æ€æžšä¸¾ï¼ˆçº¿æ€§æµè½¬ï¼šå¾…å‘å¸ƒâ†’å¾…ä¸Šä¼ â†’å¾…å®¡æ ¸â†’å®¡æ‰¹ä¸­â†’å¾…å½’æ¡£â†’å·²å½’æ¡£ï¼‰ */
export const MesPdProjectStatusEnum = {
  PENDING_PUBLISH: 0, // å¾…发布
  PENDING_UPLOAD: 1, // å¾…上传
  PENDING_AUDIT: 2, // å¾…审核
  APPROVING: 5, // å®¡æ‰¹ä¸­
  PENDING_ARCHIVE: 3, // å¾…å½’æ¡£
  ARCHIVED: 4, // å·²å½’æ¡£
} as const;
src/views/mes/pd/project/detail/index.vue
@@ -13,8 +13,8 @@
import {
  archivePdProject,
  auditPdProject,
  getPdProject,
  getPdProjectApproveProcessList,
  publishPdProject,
  uploadPdProject,
} from '#/api/mes/pd/project';
@@ -25,6 +25,7 @@
import Form from '../modules/form.vue';
import DocumentAuditList from '../modules/document-audit-list.vue';
import DocumentList from '../modules/document-list.vue';
import ProcessSelectModal from '../modules/process-select-modal.vue';
import WorkflowTimeline from '../modules/workflow-timeline.vue';
import TaskDraftBanner from '../modules/task-draft-banner.vue';
import { useDetailSchema } from './data';
@@ -50,6 +51,9 @@
const isPendingArchive = computed(
  () => project.value.status === MesPdProjectStatusEnum.PENDING_ARCHIVE,
);
const isApproving = computed(
  () => project.value.status === MesPdProjectStatusEnum.APPROVING,
);
const isArchived = computed(
  () => project.value.status === MesPdProjectStatusEnum.ARCHIVED,
);
@@ -70,6 +74,11 @@
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
const [ProcessModal, processModalApi] = useVbenModal({
  connectedComponent: ProcessSelectModal,
  destroyOnClose: true,
});
@@ -119,16 +128,38 @@
  }
}
async function handleAudit() {
  const hideLoading = message.loading({ content: '审核中…', duration: 0 });
/** æäº¤å®¡æ‰¹ - å…ˆèŽ·å–æµç¨‹åˆ—è¡¨ */
async function handleSubmit() {
  const hideLoading = message.loading({ content: '获取审批流程...', duration: 0 });
  try {
    await auditPdProject(projectId.value);
    message.success('审核通过,进入待归档状态');
    tabs.closeCurrentTab();
    router.back();
  } finally {
    const processList = await getPdProjectApproveProcessList();
    hideLoading();
    if (!processList || processList.length === 0) {
      message.warning('暂无可用审批流程,请先配置 BPM æµç¨‹');
      return;
    }
    // æ‰“开流程选择弹窗
    processModalApi.setData({ id: projectId.value, processList }).open();
  } catch {
    hideLoading();
  }
}
/** æŸ¥çœ‹å®¡æ‰¹è¯¦æƒ… */
function handleProcessDetail() {
  if (!project.value.processInstanceId) {
    message.warning('该项目尚未提交审批');
    return;
  }
  router.push({
    name: 'BpmProcessInstanceDetail',
    query: { id: project.value.processInstanceId },
  });
}
/** æäº¤å®¡æ‰¹æˆåŠŸåŽåˆ·æ–°è¯¦æƒ… */
function handleSubmitSuccess() {
  loadProjectDetail();
}
async function handleArchive() {
@@ -155,6 +186,7 @@
    :title="project.taskName || '设计任务详情'"
  >
    <FormModal @success="loadProjectDetail" />
    <ProcessModal @success="handleSubmitSuccess" />
    <template #extra>
      <TableAction
@@ -185,10 +217,16 @@
            onClick: handleUpload,
          },
          {
            label: '审核通过',
            label: '提交审批',
            type: 'primary',
            ifShow: isPendingAudit,
            onClick: handleAudit,
            onClick: handleSubmit,
          },
          {
            label: '查看审批',
            type: 'primary',
            ifShow: isApproving,
            onClick: handleProcessDetail,
          },
          {
            label: '归档',
src/views/mes/pd/project/index.vue
@@ -17,6 +17,7 @@
  deletePdProject,
  exportPdProject,
  getPdProjectApproveProcessList,
  getPdProjectPage,
  publishPdProject,
@@ -26,6 +27,7 @@
import { useGridColumns, useGridFormSchema } from './data';
import CreateForm from './modules/create-form.vue';
import Form from './modules/form.vue';
import ProcessSelectModal from './modules/process-select-modal.vue';
const router = useRouter();
@@ -36,6 +38,11 @@
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
const [ProcessModal, processModalApi] = useVbenModal({
  connectedComponent: ProcessSelectModal,
  destroyOnClose: true,
});
@@ -71,6 +78,38 @@
  } finally {
    hideLoading();
  }
}
/** æäº¤å®¡æ‰¹ - å…ˆèŽ·å–æµç¨‹åˆ—è¡¨ */
async function handleSubmit(row: MesPdProjectApi.Project) {
  const hideLoading = message.loading({
    content: '获取审批流程...',
    duration: 0,
  });
  try {
    const processList = await getPdProjectApproveProcessList();
    hideLoading();
    if (!processList || processList.length === 0) {
      message.warning('暂无可用审批流程,请先配置 BPM æµç¨‹');
      return;
    }
    // æ‰“开流程选择弹窗
    processModalApi.setData({ id: row.id, processList }).open();
  } catch {
    hideLoading();
  }
}
/** æŸ¥çœ‹å®¡æ‰¹è¯¦æƒ… */
function handleProcessDetail(row: MesPdProjectApi.Project) {
  if (!row.processInstanceId) {
    message.warning('该项目尚未提交审批');
    return;
  }
  router.push({
    name: 'BpmProcessInstanceDetail',
    query: { id: row.processInstanceId },
  });
}
async function handleArchive(row: MesPdProjectApi.Project) {
@@ -137,6 +176,7 @@
  <Page auto-content-height>
    <CreateFormModal @success="handleCreateSuccess" />
    <FormModal @success="handleRefresh" />
    <ProcessModal @success="handleRefresh" />
    <Grid table-title="设计任务">
      <template #toolbar-tools>
@@ -183,11 +223,18 @@
              onClick: () => handleDetail(row),
            },
            {
              label: '审核',
              label: '提交审批',
              type: 'link',
              auth: ['mes:pd-project:audit'],
              auth: ['mes:pd-project:update'],
              ifShow: row.status === MesPdProjectStatusEnum.PENDING_AUDIT,
              onClick: () => handleDetail(row),
              onClick: handleSubmit.bind(null, row),
            },
            {
              label: '查看审批',
              type: 'link',
              auth: ['mes:pd-project:update'],
              ifShow: row.status === MesPdProjectStatusEnum.APPROVING,
              onClick: handleProcessDetail.bind(null, row),
            },
            {
              label: '归档',
src/views/mes/pd/project/modules/process-select-modal.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,75 @@
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { message, Select } from 'ant-design-vue';
import { submitPdProjectApproval } from '#/api/mes/pd/project';
interface ProcessDefinition {
  id: string;
  name: string;
  key: string;
}
const emit = defineEmits(['success']);
const projectId = ref<number>();
const processList = ref<ProcessDefinition[]>([]);
const selectedProcessKey = ref<string>('');
const processOptions = computed(() => {
  return processList.value.map((p) => ({
    label: p.name,
    value: p.key,
  }));
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    if (!selectedProcessKey.value) {
      message.warning('请选择审批流程');
      return;
    }
    modalApi.lock();
    try {
      await submitPdProjectApproval(projectId.value!, selectedProcessKey.value);
      await modalApi.close();
      emit('success');
      message.success('提交审批成功');
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      projectId.value = undefined;
      selectedProcessKey.value = '';
      processList.value = [];
      return;
    }
    const data = modalApi.getData<{ id: number; processList: ProcessDefinition[] }>();
    projectId.value = data.id;
    processList.value = data.processList || [];
    // é»˜è®¤é€‰æ‹©ç¬¬ä¸€ä¸ªæµç¨‹
    if (processList.value.length > 0) {
      selectedProcessKey.value = processList.value[0].key;
    }
  },
});
</script>
<template>
  <Modal title="选择审批流程" class="w-1/3">
    <div class="mb-4">
      <span class="mr-2">审批流程:</span>
      <Select
        v-model:value="selectedProcessKey"
        :options="processOptions"
        style="width: 200px"
        placeholder="请选择审批流程"
      />
    </div>
  </Modal>
</template>
src/views/mes/pd/project/modules/workflow-timeline.vue
@@ -3,22 +3,35 @@
import { Steps } from 'ant-design-vue';
const STEPS = ['待发布', '待上传', '待审核', '待归档', '已归档'] as const;
import { MesPdProjectStatusEnum } from '#/packages/constants/src';
const STEPS = ['待发布', '待上传', '待审核', '审批中', '待归档', '已归档'] as const;
// çŠ¶æ€å€¼ â†’ æ—¶é—´çº¿æ­¥éª¤ç´¢å¼•(审批中=5 æ’在待审核之后)
const STATUS_ORDER = [
  MesPdProjectStatusEnum.PENDING_PUBLISH,
  MesPdProjectStatusEnum.PENDING_UPLOAD,
  MesPdProjectStatusEnum.PENDING_AUDIT,
  MesPdProjectStatusEnum.APPROVING,
  MesPdProjectStatusEnum.PENDING_ARCHIVE,
  MesPdProjectStatusEnum.ARCHIVED,
];
const props = defineProps<{
  /** é¡¹ç›®å½“前状态 (0-4) */
  /** é¡¹ç›®å½“前状态 */
  status: number;
}>();
const current = computed(() => {
  // çŠ¶æ€å€¼å³å½“å‰å·²å®Œæˆåˆ°ç¬¬å‡ æ­¥ï¼š0=第一步进行中,4=全部完成
  return props.status;
  // æ ¹æ®çŠ¶æ€å€¼æ˜ å°„åˆ°æ—¶é—´çº¿æ­¥éª¤ç´¢å¼•
  const index = STATUS_ORDER.indexOf(props.status);
  return index >= 0 ? index : 0;
});
const nodes = computed(() =>
  STEPS.map((name, index) => ({
    title: name,
    status: index < props.status ? 'finish' : index === props.status ? 'process' : 'wait',
    status: index < current.value ? 'finish' : index === current.value ? 'process' : 'wait',
  })),
);
</script>
src/views/mes/qc/batchtrace/data.ts
@@ -5,6 +5,8 @@
import { markRaw } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { MdClientSelect } from '#/views/mes/md/client/components';
import { MdItemSelect } from '#/views/mes/md/item/components';
import { MdVendorSelect } from '#/views/mes/md/vendor/components';
@@ -227,3 +229,176 @@
    },
  ];
}
/** é‡‡è´­æ¥æºåˆ—表的字段 */
export function usePurchaseSourceColumns(): VxeTableGridOptions<MesWmBatchApi.PurchaseSource>['columns'] {
  return [
    {
      field: 'receiptCode',
      title: '采购入库单号',
      minWidth: 160,
    },
    {
      field: 'receiptDate',
      title: '入库日期',
      minWidth: 160,
      formatter: 'formatDateTime',
    },
    {
      field: 'purchaseOrderCode',
      title: '采购订单编号',
      minWidth: 160,
    },
    {
      field: 'vendorCode',
      title: '供应商编码',
      minWidth: 140,
    },
    {
      field: 'vendorName',
      title: '供应商名称',
      minWidth: 140,
    },
    {
      field: 'itemCode',
      title: '物料编码',
      minWidth: 140,
    },
    {
      field: 'itemName',
      title: '物料名称',
      minWidth: 160,
    },
    {
      field: 'iqcCode',
      title: 'IQC æ£€éªŒå•号',
      minWidth: 160,
    },
    {
      field: 'checkResult',
      title: 'IQC æ£€éªŒç»“论',
      width: 110,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_QC_CHECK_RESULT },
      },
    },
    {
      field: 'receivedQuantity',
      title: '入库数量',
      width: 100,
    },
  ];
}
/** é”€å”®åŽ»å‘åˆ—è¡¨çš„å­—æ®µ */
export function useSalesTargetColumns(): VxeTableGridOptions<MesWmBatchApi.SalesTarget>['columns'] {
  return [
    {
      field: 'salesCode',
      title: '销售出库单号',
      minWidth: 160,
    },
    {
      field: 'salesDate',
      title: '出库日期',
      minWidth: 160,
      formatter: 'formatDateTime',
    },
    {
      field: 'salesOrderCode',
      title: '销售订单编号',
      minWidth: 160,
    },
    {
      field: 'clientName',
      title: '客户名称',
      minWidth: 140,
    },
    {
      field: 'itemCode',
      title: '物料编码',
      minWidth: 140,
    },
    {
      field: 'itemName',
      title: '物料名称',
      minWidth: 160,
    },
    {
      field: 'oqcCode',
      title: 'OQC æ£€éªŒå•号',
      minWidth: 160,
    },
    {
      field: 'checkResult',
      title: 'OQC æ£€éªŒç»“论',
      width: 110,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_QC_CHECK_RESULT },
      },
    },
    {
      field: 'quantity',
      title: '出库数量',
      width: 100,
    },
    {
      field: 'qualityStatus',
      title: '质量状态',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_WM_QUALITY_STATUS },
      },
    },
  ];
}
/** å·¥åºæµè½¬åˆ—表的字段 */
export function useProcessTraceColumns(): VxeTableGridOptions<MesWmBatchApi.ProcessTrace>['columns'] {
  return [
    {
      field: 'eventType',
      title: '事件类型',
      width: 90,
      slots: { default: 'eventType' },
    },
    {
      field: 'workOrderCode',
      title: '生产工单号',
      minWidth: 160,
    },
    {
      field: 'processCode',
      title: '工序编码',
      minWidth: 120,
    },
    {
      field: 'processName',
      title: '工序名称',
      minWidth: 140,
    },
    {
      field: 'quantity',
      title: '数量',
      width: 100,
    },
    {
      field: 'qualityStatus',
      title: '质量状态',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.MES_WM_QUALITY_STATUS },
      },
    },
    {
      field: 'eventTime',
      title: '事件时间',
      minWidth: 160,
      formatter: 'formatDateTime',
    },
  ];
}
src/views/mes/qc/batchtrace/index.vue
@@ -4,8 +4,10 @@
import { Page, useVbenModal } from '@vben/common-ui';
import { Input, message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getBatchPage } from '#/api/mes/wm/batch';
import { getBatchBySn, getBatchPage } from '#/api/mes/wm/batch';
import { useGridColumns, useGridFormSchema } from './data';
import TraceDetail from './modules/trace-detail.vue';
@@ -18,6 +20,19 @@
/** æ‰“开批次追溯详情 */
function handleTrace(row: MesWmBatchApi.Batch) {
  detailModalApi.setData(row).open();
}
/** æ ¹æ® SN åæŸ¥æ‰¹æ¬¡å¹¶æ‰“开详情 */
async function handleSearchBySn(sn: string) {
  if (!sn) {
    return;
  }
  const batch = await getBatchBySn(sn.trim());
  if (batch) {
    handleTrace(batch);
  } else {
    message.warning(`未找到 SN「${sn}」对应的批次`);
  }
}
const [Grid] = useVbenVxeGrid({
@@ -54,6 +69,15 @@
<template>
  <Page auto-content-height><DetailModal />
    <div class="mb-3 w-80">
      <Input.Search
        placeholder="输入 SN åºåˆ—号反查批次"
        enter-button="反查"
        allow-clear
        @search="handleSearchBySn"
      />
    </div>
    <Grid table-title="批次追溯列表">
      <template #actions="{ row }">
        <TableAction
src/views/mes/qc/batchtrace/modules/process-trace-list.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,66 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmBatchApi } from '#/api/mes/wm/batch';
import { watch } from 'vue';
import { Tag } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getProcessTraceList } from '#/api/mes/wm/batch';
import { useProcessTraceColumns } from '../data';
const props = defineProps<{
  batchCode?: string;
}>();
const [Grid, gridApi] = useVbenVxeGrid({
  gridOptions: {
    columns: useProcessTraceColumns(),
    minHeight: 240,
    pagerConfig: {
      enabled: false,
    },
    rowConfig: {
      isHover: true,
    },
    showOverflow: true,
    toolbarConfig: {
      enabled: false,
    },
  } as VxeTableGridOptions<MesWmBatchApi.ProcessTrace>,
});
/** åŠ è½½å·¥åºæµè½¬åˆ—è¡¨ */
async function getList() {
  if (!props.batchCode) {
    await gridApi.grid?.loadData([]);
    return;
  }
  gridApi.setLoading(true);
  try {
    const list = await getProcessTraceList(props.batchCode);
    await gridApi.grid?.loadData(list);
  } finally {
    gridApi.setLoading(false);
  }
}
watch(
  () => props.batchCode,
  () => {
    getList();
  },
  { immediate: true },
);
</script>
<template>
  <Grid class="w-full">
    <template #eventType="{ row }">
      <Tag v-if="row.eventType === 'OUT'" color="success">产出</Tag>
      <Tag v-else color="processing">投入</Tag>
    </template>
  </Grid>
</template>
src/views/mes/qc/batchtrace/modules/purchase-source-list.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,59 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmBatchApi } from '#/api/mes/wm/batch';
import { watch } from 'vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getPurchaseSourceList } from '#/api/mes/wm/batch';
import { usePurchaseSourceColumns } from '../data';
const props = defineProps<{
  batchCode?: string;
}>();
const [Grid, gridApi] = useVbenVxeGrid({
  gridOptions: {
    columns: usePurchaseSourceColumns(),
    minHeight: 240,
    pagerConfig: {
      enabled: false,
    },
    rowConfig: {
      isHover: true,
    },
    showOverflow: true,
    toolbarConfig: {
      enabled: false,
    },
  } as VxeTableGridOptions<MesWmBatchApi.PurchaseSource>,
});
/** åŠ è½½é‡‡è´­æ¥æºåˆ—è¡¨ */
async function getList() {
  if (!props.batchCode) {
    await gridApi.grid?.loadData([]);
    return;
  }
  gridApi.setLoading(true);
  try {
    const list = await getPurchaseSourceList(props.batchCode);
    await gridApi.grid?.loadData(list);
  } finally {
    gridApi.setLoading(false);
  }
}
watch(
  () => props.batchCode,
  () => {
    getList();
  },
  { immediate: true },
);
</script>
<template>
  <Grid class="w-full" />
</template>
src/views/mes/qc/batchtrace/modules/sales-target-list.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,59 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmBatchApi } from '#/api/mes/wm/batch';
import { watch } from 'vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getSalesTargetList } from '#/api/mes/wm/batch';
import { useSalesTargetColumns } from '../data';
const props = defineProps<{
  batchCode?: string;
}>();
const [Grid, gridApi] = useVbenVxeGrid({
  gridOptions: {
    columns: useSalesTargetColumns(),
    minHeight: 240,
    pagerConfig: {
      enabled: false,
    },
    rowConfig: {
      isHover: true,
    },
    showOverflow: true,
    toolbarConfig: {
      enabled: false,
    },
  } as VxeTableGridOptions<MesWmBatchApi.SalesTarget>,
});
/** åŠ è½½é”€å”®åŽ»å‘åˆ—è¡¨ */
async function getList() {
  if (!props.batchCode) {
    await gridApi.grid?.loadData([]);
    return;
  }
  gridApi.setLoading(true);
  try {
    const list = await getSalesTargetList(props.batchCode);
    await gridApi.grid?.loadData(list);
  } finally {
    gridApi.setLoading(false);
  }
}
watch(
  () => props.batchCode,
  () => {
    getList();
  },
  { immediate: true },
);
</script>
<template>
  <Grid class="w-full" />
</template>
src/views/mes/qc/batchtrace/modules/trace-detail.vue
@@ -5,15 +5,24 @@
import { useVbenModal } from '@vben/common-ui';
import { Tabs } from 'ant-design-vue';
import { Tabs, Tooltip } from 'ant-design-vue';
import { useDescription } from '#/components/description';
import { useDetailSchema } from '../data';
import ProcessTraceList from './process-trace-list.vue';
import PurchaseSourceList from './purchase-source-list.vue';
import SalesTargetList from './sales-target-list.vue';
import TraceList from './trace-list.vue';
const detailData = ref<MesWmBatchApi.Batch>(); // å½“前批次详情
const subTabsName = ref<'backward' | 'forward'>('forward'); // å½“前激活的追溯方向
const subTabsName = ref<
  | 'backward'
  | 'forward'
  | 'purchase'
  | 'sales'
  | 'process'
>('forward'); // å½“前激活的追溯页签
const [Descriptions] = useDescription({
  bordered: true,
@@ -47,12 +56,46 @@
      class="mx-4 mt-4"
      type="card"
    >
      <Tabs.TabPane key="forward" tab="向前追溯">
      <Tabs.TabPane key="forward">
        <template #tab>
          <Tooltip title="顺着生产流往下游查:该批次被哪些工单消耗后,产出成了哪些成品批次">
            <span>向前追溯</span>
          </Tooltip>
        </template>
        <TraceList :batch-code="detailData.code" direction="forward" />
      </Tabs.TabPane>
      <Tabs.TabPane key="backward" tab="向后追溯">
      <Tabs.TabPane key="backward">
        <template #tab>
          <Tooltip title="逆着生产流往上游查:生产该批次时,消耗了哪些原料批次">
            <span>向后追溯</span>
          </Tooltip>
        </template>
        <TraceList :batch-code="detailData.code" direction="backward" />
      </Tabs.TabPane>
      <Tabs.TabPane key="purchase">
        <template #tab>
          <Tooltip title="原料侧来源:该批次对应的采购入库单与 IQC æ¥æ–™æ£€éªŒè®°å½•">
            <span>采购来源</span>
          </Tooltip>
        </template>
        <PurchaseSourceList :batch-code="detailData.code" />
      </Tabs.TabPane>
      <Tabs.TabPane key="sales">
        <template #tab>
          <Tooltip title="成品侧去向:该批次对应的销售出库单与 OQC å‡ºåŽ‚æ£€éªŒè®°å½•">
            <span>销售去向</span>
          </Tooltip>
        </template>
        <SalesTargetList :batch-code="detailData.code" />
      </Tabs.TabPane>
      <Tabs.TabPane key="process">
        <template #tab>
          <Tooltip title="各工序的投入/产出事件:该批次在哪些工序被投入或产出,及数量和质量状态">
            <span>工序流转</span>
          </Tooltip>
        </template>
        <ProcessTraceList :batch-code="detailData.code" />
      </Tabs.TabPane>
    </Tabs>
  </Modal>
</template>