From 7dfbaba5930676252d8339f7d04861c1c2351d5b Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期四, 13 八月 2026 16:27:56 +0800
Subject: [PATCH] feat(mes): 增加产品设计项目审批流程和批次追溯功能

---
 src/api/mes/wm/batch/index.ts                                |   81 ++++++
 src/views/mes/qc/batchtrace/data.ts                          |  175 +++++++++++++
 src/views/mes/qc/batchtrace/modules/purchase-source-list.vue |   59 ++++
 src/views/mes/pd/project/index.vue                           |   53 +++
 src/packages/constants/src/biz-mes-enum.ts                   |    3 
 company.config.ts                                            |    6 
 src/views/mes/pd/project/detail/index.vue                    |   58 +++
 /dev/null                                                    |    0 
 src/api/mes/pd/project/index.ts                              |   17 +
 src/views/mes/qc/batchtrace/modules/trace-detail.vue         |   51 +++
 src/views/mes/qc/batchtrace/index.vue                        |   26 +
 src/views/mes/pd/project/modules/process-select-modal.vue    |   75 +++++
 src/views/mes/qc/batchtrace/modules/process-trace-list.vue   |   66 +++++
 .env.production                                              |    4 
 src/views/mes/qc/batchtrace/modules/sales-target-list.vue    |   59 ++++
 src/views/mes/pd/project/modules/workflow-timeline.vue       |   23 +
 16 files changed, 726 insertions(+), 30 deletions(-)

diff --git a/.env.production b/.env.production
index ec0c2aa..bfe3423 100644
--- a/.env.production
+++ b/.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
 # 鏂囦欢涓婁紶绫诲瀷锛歴erver - 鍚庣涓婁紶锛� client - 鍓嶇鐩磋繛涓婁紶锛屼粎鏀寔S3鏈嶅姟
 VITE_UPLOAD_TYPE=server
 
diff --git a/company.config.ts b/company.config.ts
index f4436c3..8cde172 100644
--- a/company.config.ts
+++ b/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',
+  },
 };
diff --git a/dist.zip b/dist.zip
deleted file mode 100644
index b695091..0000000
--- a/dist.zip
+++ /dev/null
Binary files differ
diff --git a/src/api/mes/pd/project/index.ts b/src/api/mes/pd/project/index.ts
index 40e34ab..e6841ce 100644
--- a/src/api/mes/pd/project/index.ts
+++ b/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, {
diff --git a/src/api/mes/wm/batch/index.ts b/src/api/mes/wm/batch/index.ts
index 7ed63bc..569efd7 100644
--- a/src/api/mes/wm/batch/index.ts
+++ b/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'; // 浜嬩欢绫诲瀷锛歄UT-浜у嚭 / 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 },
+  });
+}
diff --git a/src/packages/constants/src/biz-mes-enum.ts b/src/packages/constants/src/biz-mes-enum.ts
index 435b1db..85c58db 100644
--- a/src/packages/constants/src/biz-mes-enum.ts
+++ b/src/packages/constants/src/biz-mes-enum.ts
@@ -530,11 +530,12 @@
   RIGHT: 2, // 鍙宠ˉ榻�
 } as const;
 
-/** MES 浜у搧璁捐椤圭洰鐘舵�佹灇涓撅紙绾挎�ф祦杞細寰呭彂甯冣啋寰呬笂浼犫啋寰呭鏍糕啋寰呭綊妗b啋宸插綊妗o級 */
+/** MES 浜у搧璁捐椤圭洰鐘舵�佹灇涓撅紙绾挎�ф祦杞細寰呭彂甯冣啋寰呬笂浼犫啋寰呭鏍糕啋瀹℃壒涓啋寰呭綊妗b啋宸插綊妗o級 */
 export const MesPdProjectStatusEnum = {
   PENDING_PUBLISH: 0, // 寰呭彂甯�
   PENDING_UPLOAD: 1, // 寰呬笂浼�
   PENDING_AUDIT: 2, // 寰呭鏍�
+  APPROVING: 5, // 瀹℃壒涓�
   PENDING_ARCHIVE: 3, // 寰呭綊妗�
   ARCHIVED: 4, // 宸插綊妗�
 } as const;
diff --git a/src/views/mes/pd/project/detail/index.vue b/src/views/mes/pd/project/detail/index.vue
index 36da5ce..97f7f8b 100644
--- a/src/views/mes/pd/project/detail/index.vue
+++ b/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: '褰掓。',
diff --git a/src/views/mes/pd/project/index.vue b/src/views/mes/pd/project/index.vue
index 24516e1..6727d6d 100644
--- a/src/views/mes/pd/project/index.vue
+++ b/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: '褰掓。',
diff --git a/src/views/mes/pd/project/modules/process-select-modal.vue b/src/views/mes/pd/project/modules/process-select-modal.vue
new file mode 100644
index 0000000..c0d099a
--- /dev/null
+++ b/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>
diff --git a/src/views/mes/pd/project/modules/workflow-timeline.vue b/src/views/mes/pd/project/modules/workflow-timeline.vue
index 6989422..db03cc8 100644
--- a/src/views/mes/pd/project/modules/workflow-timeline.vue
+++ b/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>
diff --git a/src/views/mes/qc/batchtrace/data.ts b/src/views/mes/qc/batchtrace/data.ts
index 52e9620..3013c2e 100644
--- a/src/views/mes/qc/batchtrace/data.ts
+++ b/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',
+    },
+  ];
+}
diff --git a/src/views/mes/qc/batchtrace/index.vue b/src/views/mes/qc/batchtrace/index.vue
index b403dec..1762b35 100644
--- a/src/views/mes/qc/batchtrace/index.vue
+++ b/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
diff --git a/src/views/mes/qc/batchtrace/modules/process-trace-list.vue b/src/views/mes/qc/batchtrace/modules/process-trace-list.vue
new file mode 100644
index 0000000..2c3725e
--- /dev/null
+++ b/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>
diff --git a/src/views/mes/qc/batchtrace/modules/purchase-source-list.vue b/src/views/mes/qc/batchtrace/modules/purchase-source-list.vue
new file mode 100644
index 0000000..472a6a4
--- /dev/null
+++ b/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>
diff --git a/src/views/mes/qc/batchtrace/modules/sales-target-list.vue b/src/views/mes/qc/batchtrace/modules/sales-target-list.vue
new file mode 100644
index 0000000..0963502
--- /dev/null
+++ b/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>
diff --git a/src/views/mes/qc/batchtrace/modules/trace-detail.vue b/src/views/mes/qc/batchtrace/modules/trace-detail.vue
index 7fcba7e..8d01d5b 100644
--- a/src/views/mes/qc/batchtrace/modules/trace-detail.vue
+++ b/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>

--
Gitblit v1.9.3