From 51bdfbf624f857f125ed55d42b17bb85aa526b97 Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期四, 23 七月 2026 17:48:44 +0800
Subject: [PATCH] 银川 1.生产订单新增修改时候工序列表拖拽问题解决 2.生产订单新增修改时候工序可以新增物料并更新

---
 src/views/mes/pro/workorder/modules/process-list.vue |  201 ++++++++++++++++++++++++++++++++++++++++---------
 1 files changed, 163 insertions(+), 38 deletions(-)

diff --git a/src/views/mes/pro/workorder/modules/process-list.vue b/src/views/mes/pro/workorder/modules/process-list.vue
index f5b0cdc..fd2a7e7 100644
--- a/src/views/mes/pro/workorder/modules/process-list.vue
+++ b/src/views/mes/pro/workorder/modules/process-list.vue
@@ -11,8 +11,10 @@
 import { MesProWorkOrderStatusEnum } from '@vben/constants';
 import { IconifyIcon } from '@vben/icons';
 
-import { useSortable } from '@vueuse/integrations/useSortable';
-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 {
@@ -21,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';
@@ -47,8 +50,16 @@
 
 const isCreateMode = computed(() => props.formType === 'create');
 
-// 鎷栨嫿鎺掑簭
-const sortableInstance = ref<any>(null);
+// 鎷栨嫿鎺掑簭瀹炰緥
+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,
@@ -65,51 +76,57 @@
     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();
-  sortableInstance.value = useSortable(
-    '.workorder-process-list .vxe-table--body-wrapper:not(.fixed-right--wrapper) .vxe-table--body tbody',
-    list.value,
-    {
-      animation: 150,
-      draggable: '.vxe-body--row',
-      handle: '.drag-handle',
-      onEnd: ({ newDraggableIndex, oldDraggableIndex }) => {
-        if (
-          newDraggableIndex !== undefined &&
-          oldDraggableIndex !== undefined &&
-          oldDraggableIndex !== newDraggableIndex
-        ) {
-          // 閲嶆柊鎺掑簭鏁扮粍
-          list.value.splice(
-            newDraggableIndex,
-            0,
-            list.value.splice(oldDraggableIndex, 1)[0]!,
-          );
-          // 鏇存柊 sort 瀛楁
-          list.value.forEach((item, index) => {
-            item.sort = index + 1;
-          });
-          gridApi.setGridOptions({ data: list.value });
-          emit('processListChange', list.value);
-        }
-      },
-    },
+  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.value) {
-    sortableInstance.value.stop?.();
-    sortableInstance.value = null;
+  if (sortableInstance) {
+    sortableInstance.destroy();
+    sortableInstance = null;
   }
 }
 
@@ -230,6 +247,81 @@
   }
 }
 
+/** 鎵撳紑鏂板鐗╂枡寮圭獥 */
+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();
@@ -248,6 +340,32 @@
 <template>
   <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="[
@@ -282,6 +400,11 @@
         <TableAction
           :actions="[
             {
+              label: '鏂板鐗╂枡',
+              type: 'link',
+              onClick: handleMaterialInput.bind(null, row),
+            },
+            {
               label: $t('common.edit'),
               type: 'link',
               ifShow: isEditable,
@@ -300,7 +423,7 @@
           ]"
         />
       </template>
-      <template #expand="{ row }">
+      <template #expand_content="{ row }">
         <div
           v-if="row.bomItems && row.bomItems.length > 0"
           class="bg-gray-50 px-8 py-3"
@@ -310,7 +433,9 @@
             <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>
@@ -320,4 +445,4 @@
       </template>
     </Grid>
   </div>
-</template>
\ No newline at end of file
+</template>

--
Gitblit v1.9.3