From fa42bcb7396e06e335bbbff67f5898b98f8d9aa0 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期五, 24 七月 2026 17:09:28 +0800
Subject: [PATCH] refactor(mes): 移除工单模块中的BOM相关功能

---
 src/views/mes/pro/workorder/modules/process-list.vue |  260 +++++++++++++++++++++++++++++++++++++++++++++++----
 1 files changed, 236 insertions(+), 24 deletions(-)

diff --git a/src/views/mes/pro/workorder/modules/process-list.vue b/src/views/mes/pro/workorder/modules/process-list.vue
index 6559581..fd2a7e7 100644
--- a/src/views/mes/pro/workorder/modules/process-list.vue
+++ b/src/views/mes/pro/workorder/modules/process-list.vue
@@ -5,12 +5,16 @@
 import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder';
 import type { MesProWorkOrderProcessApi } from '#/api/mes/pro/workorder/process';
 
-import { computed, ref, watch } from 'vue';
+import { computed, nextTick, onUnmounted, ref, watch } from 'vue';
 
 import { useVbenModal } from '@vben/common-ui';
 import { MesProWorkOrderStatusEnum } from '@vben/constants';
+import { IconifyIcon } from '@vben/icons';
 
-import { message, Tag } from 'ant-design-vue';
+import type { MesMdItemApi } from '#/api/mes/md/item';
+
+import Sortable from 'sortablejs';
+import { Button, InputNumber, message, Modal, Tag } from 'ant-design-vue';
 
 import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
 import {
@@ -19,6 +23,7 @@
 } from '#/api/mes/pro/workorder/process';
 import { getRouteProcessListByProduct } from '#/api/mes/pro/route/process';
 import { $t } from '#/locales';
+import { MdItemSelectDialog } from '#/views/mes/md/item/components';
 
 import { useProcessGridColumns } from '../data';
 import ProcessForm from './process-form.vue';
@@ -45,6 +50,17 @@
 
 const isCreateMode = computed(() => props.formType === 'create');
 
+// 鎷栨嫿鎺掑簭瀹炰緥
+let sortableInstance: Sortable | null = null;
+
+// 鐗╂枡鎶曞叆
+const itemSelectDialogRef = ref<InstanceType<typeof MdItemSelectDialog>>();
+const currentMaterialRow = ref<MesProWorkOrderProcessApi.WorkOrderProcess>();
+
+// 鐢ㄦ枡姣斾緥寮圭獥
+const quantityModalOpen = ref(false);
+const pendingItems = ref<(MesMdItemApi.Item & { quantity: number })[]>([]);
+
 const [ProcessFormModal, processFormModalApi] = useVbenModal({
   connectedComponent: ProcessForm,
   destroyOnClose: true,
@@ -54,15 +70,65 @@
   gridOptions: {
     autoResize: true,
     border: true,
-    columns: useProcessGridColumns(),
+    columns: useProcessGridColumns(isEditable.value),
     data: [],
     minHeight: 240,
     pagerConfig: { enabled: false },
     rowConfig: { isHover: true, keyField: 'processId' },
     showOverflow: true,
+    expandConfig: {
+      padding: true,
+    },
     toolbarConfig: { enabled: false },
   } as VxeTableGridOptions<MesProWorkOrderProcessApi.WorkOrderProcess>,
 });
+
+/** 鍒濆鍖栨嫋鎷芥帓搴� */
+async function initSortable() {
+  destroySortable();
+  if (!isEditable.value || list.value.length === 0) return;
+  await nextTick();
+  const el = document.querySelector<HTMLElement>(
+    '.workorder-process-list .vxe-table--body-wrapper.body--wrapper .vxe-table--body tbody',
+  );
+  if (!el) return;
+  sortableInstance = Sortable.create(el, {
+    animation: 150,
+    draggable: '.vxe-body--row',
+    handle: '.drag-handle',
+    onEnd: (evt) => {
+      const { oldIndex, newIndex, item } = evt;
+      if (
+        oldIndex === undefined ||
+        newIndex === undefined ||
+        oldIndex === newIndex
+      ) {
+        return;
+      }
+      // 杩樺師 DOM锛歋ortable 宸茬Щ鍔ㄨ妭鐐癸紝闇�鏀惧洖鍘熶綅锛岀敱鏁版嵁椹卞姩閲嶆覆鏌�
+      if (item && oldIndex < el.children.length) {
+        const refNode = el.children[oldIndex] || null;
+        el.insertBefore(item, refNode);
+      }
+      // 鏇存柊鏁版嵁椤哄簭
+      list.value.splice(newIndex, 0, list.value.splice(oldIndex, 1)[0]!);
+      // 鏇存柊 sort 瀛楁
+      list.value.forEach((p, index) => {
+        p.sort = index + 1;
+      });
+      gridApi.setGridOptions({ data: list.value });
+      emit('processListChange', list.value);
+    },
+  });
+}
+
+/** 閿�姣佹嫋鎷芥帓搴忓疄渚� */
+function destroySortable() {
+  if (sortableInstance) {
+    sortableInstance.destroy();
+    sortableInstance = null;
+  }
+}
 
 /** 鍔犺浇宸ュ簭鍒楄〃 */
 async function getList() {
@@ -94,6 +160,7 @@
           outputItemCode: item.outputItemCode,
           outputItemName: item.outputItemName,
           remark: item.remark,
+          bomItems: item.bomItems,
         }));
       }
     } else {
@@ -106,6 +173,8 @@
     }
     gridApi.setGridOptions({ data: list.value });
     emit('processListChange', list.value);
+    // 鍒濆鍖栨嫋鎷芥帓搴�
+    initSortable();
   } finally {
     gridApi.setLoading(false);
   }
@@ -135,25 +204,6 @@
     .open();
 }
 
-/** 鍒犻櫎宸ュ簭 */
-function handleDelete(row: MesProWorkOrderProcessApi.WorkOrderProcess) {
-  if (isCreateMode.value) {
-    // 鏂板妯″紡锛氫粠鍒楄〃鍒犻櫎
-    const index = list.value.findIndex((item) => item.processId === row.processId);
-    if (index > -1) {
-      list.value.splice(index, 1);
-      gridApi.setGridOptions({ data: list.value });
-      emit('processListChange', list.value);
-    }
-  } else {
-    // 缂栬緫妯″紡锛氳皟鐢ㄦ帴鍙e垹闄�
-    deleteWorkOrderProcess(row.id!).then(() => {
-      message.success($t('ui.actionMessage.deleteSuccess', [row.processName]));
-      getList();
-    });
-  }
-}
-
 /** 鏂板/缂栬緫鎴愬姛鍥炶皟 */
 function handleFormSuccess(data: MesProWorkOrderProcessApi.WorkOrderProcess, isEdit: boolean) {
   if (isCreateMode.value) {
@@ -167,10 +217,115 @@
     }
     gridApi.setGridOptions({ data: list.value });
     emit('processListChange', list.value);
+    initSortable();
   } else {
     getList();
   }
 }
+
+/** 鍒犻櫎宸ュ簭 */
+function handleDelete(row: MesProWorkOrderProcessApi.WorkOrderProcess) {
+  if (isCreateMode.value) {
+    // 鏂板妯″紡锛氫粠鍒楄〃鍒犻櫎
+    const index = list.value.findIndex((item) => item.processId === row.processId);
+    if (index > -1) {
+      list.value.splice(index, 1);
+      // 鏇存柊 sort 瀛楁
+      list.value.forEach((item, idx) => {
+        item.sort = idx + 1;
+      });
+      gridApi.setGridOptions({ data: list.value });
+      emit('processListChange', list.value);
+      initSortable();
+    }
+  } else {
+    // 缂栬緫妯″紡锛氳皟鐢ㄦ帴鍙e垹闄�
+    deleteWorkOrderProcess(row.id!).then(() => {
+      message.success($t('ui.actionMessage.deleteSuccess', [row.processName]));
+      getList();
+    });
+  }
+}
+
+/** 鎵撳紑鏂板鐗╂枡寮圭獥 */
+function handleMaterialInput(row: MesProWorkOrderProcessApi.WorkOrderProcess) {
+  currentMaterialRow.value = row;
+  const selectedIds = (row.bomItems || [])
+    .map((item) => item.itemId)
+    .filter((id): id is number => id !== undefined);
+  itemSelectDialogRef.value?.open(selectedIds, { multiple: true });
+}
+
+/** 鐗╂枡閫変腑鍥炶皟锛氬幓閲嶅悗鎵撳紑鐢ㄦ枡姣斾緥濉啓寮圭獥 */
+function handleItemSelected(rows: MesMdItemApi.Item[]) {
+  const row = currentMaterialRow.value;
+  if (!row || rows.length === 0) return;
+  const existingIds = new Set(
+    (row.bomItems || [])
+      .map((item) => item.itemId)
+      .filter((id): id is number => id !== undefined),
+  );
+  const newItems = rows.filter((item) => item.id && !existingIds.has(item.id));
+  if (newItems.length === 0) {
+    message.warning('鎵�閫夌墿鏂欏潎宸插瓨鍦�');
+    return;
+  }
+  pendingItems.value = newItems.map((item) => ({ ...item, quantity: 1 }));
+  quantityModalOpen.value = true;
+}
+
+/** 纭鐢ㄦ枡姣斾緥锛屾坊鍔犲埌 bomItems */
+function handleQuantityConfirm() {
+  const row = currentMaterialRow.value;
+  if (!row) return;
+  // 鍦� list 涓壘鍒板搴旇骞舵浛鎹紝纭繚鍝嶅簲寮忔洿鏂�
+  const rowIndex = list.value.findIndex(
+    (item) => item.processId === row.processId,
+  );
+  if (rowIndex === -1) return;
+  const target = list.value[rowIndex]!;
+  const newBomItems = [
+    ...(target.bomItems || []),
+    ...pendingItems.value.map((item) => ({
+      itemId: item.id,
+      itemCode: item.code,
+      itemName: item.name,
+      quantity: item.quantity,
+      remark: undefined,
+    })),
+  ];
+  // 鍒涘缓鏂扮殑 row 瀵硅薄鍜屾柊鐨� list 鏁扮粍锛屽己鍒跺搷搴斿紡鏇存柊
+  list.value[rowIndex] = { ...target, bomItems: newBomItems };
+  quantityModalOpen.value = false;
+  pendingItems.value = [];
+  gridApi.setGridOptions({ data: list.value });
+  emit('processListChange', list.value);
+  message.success('鐗╂枡娣诲姞鎴愬姛');
+}
+
+/** 鍒犻櫎鎶曟枡鏄庣粏 */
+function handleRemoveBomItem(
+  row: MesProWorkOrderProcessApi.WorkOrderProcess,
+  index: number,
+) {
+  const rowIndex = list.value.findIndex(
+    (item) => item.processId === row.processId,
+  );
+  if (rowIndex === -1) return;
+  const target = list.value[rowIndex]!;
+  if (!target.bomItems) return;
+  list.value[rowIndex] = {
+    ...target,
+    bomItems: target.bomItems.filter((_, i) => i !== index),
+  };
+  gridApi.setGridOptions({ data: list.value });
+  emit('processListChange', list.value);
+}
+
+// 缁勪欢鍗歌浇鏃堕攢姣佹帓搴忓疄渚�
+onUnmounted(() => {
+  destroySortable();
+});
 
 // 鐩戝惉浜у搧/宸ュ崟鍙樺寲
 watch(
@@ -183,8 +338,34 @@
 </script>
 
 <template>
-  <div>
+  <div class="workorder-process-list">
     <ProcessFormModal @success="handleFormSuccess" />
+    <MdItemSelectDialog ref="itemSelectDialogRef" @selected="handleItemSelected" />
+    <Modal
+      v-model:open="quantityModalOpen"
+      title="濉啓鐢ㄦ枡姣斾緥"
+      width="500px"
+      @ok="handleQuantityConfirm"
+    >
+      <div class="space-y-3 py-2">
+        <div
+          v-for="(item, idx) in pendingItems"
+          :key="item.id"
+          class="flex items-center gap-3"
+        >
+          <span class="shrink-0 text-sm text-gray-700">
+            {{ item.name || item.code }}
+          </span>
+          <InputNumber
+            v-model:value="item.quantity"
+            :min="0.01"
+            :precision="2"
+            class="!w-32"
+          />
+          <span class="text-xs text-gray-400">鐢ㄩ噺姣斾緥</span>
+        </div>
+      </div>
+    </Modal>
     <div v-if="isEditable" class="mb-3 flex items-center justify-start">
       <TableAction
         :actions="[
@@ -197,6 +378,12 @@
       />
     </div>
     <Grid class="w-full" table-title="宸ュ簭">
+      <template #dragHandle>
+        <IconifyIcon
+          icon="ic:round-drag-indicator"
+          class="drag-handle cursor-move text-xl text-gray-400"
+        />
+      </template>
       <template #keyFlag="{ row }">
         <Tag v-if="row.keyFlag" color="red">鍏抽敭</Tag>
         <span v-else>-</span>
@@ -212,6 +399,11 @@
       <template #actions="{ row }">
         <TableAction
           :actions="[
+            {
+              label: '鏂板鐗╂枡',
+              type: 'link',
+              onClick: handleMaterialInput.bind(null, row),
+            },
             {
               label: $t('common.edit'),
               type: 'link',
@@ -231,6 +423,26 @@
           ]"
         />
       </template>
+      <template #expand_content="{ row }">
+        <div
+          v-if="row.bomItems && row.bomItems.length > 0"
+          class="bg-gray-50 px-8 py-3"
+        >
+          <div class="text-xs text-gray-500 mb-2 font-medium">鎶曟枡鏄庣粏</div>
+          <div class="flex flex-wrap gap-2">
+            <Tag
+              v-for="(bom, idx) in row.bomItems"
+              :key="idx"
+              closable
+              color="blue"
+              @close="handleRemoveBomItem(row, idx)"
+            >
+              {{ bom.itemName || bom.itemCode }}
+              <span class="text-gray-400 ml-1">脳{{ bom.quantity }}</span>
+            </Tag>
+          </div>
+        </div>
+      </template>
     </Grid>
   </div>
-</template>
\ No newline at end of file
+</template>

--
Gitblit v1.9.3