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.生产订单新增修改时候工序可以新增物料并更新

---
 vite.config.ts                                       |   15 ++
 pnpm-workspace.yaml                                  |    8 
 src/views/mes/pro/workorder/modules/form.vue         |    6 
 package.json                                         |    1 
 src/views/mes/pro/workorder/modules/process-list.vue |  201 ++++++++++++++++++++++++++++++++-------
 src/views/mes/pro/workorder/data.ts                  |    4 
 pnpm-lock.yaml                                       |   12 +
 7 files changed, 196 insertions(+), 51 deletions(-)

diff --git a/package.json b/package.json
index 4dbcf6f..bbc683e 100644
--- a/package.json
+++ b/package.json
@@ -63,6 +63,7 @@
     "highlight.js": "catalog:",
     "livekit-client": "^2.20.0",
     "pinia": "catalog:",
+    "sortablejs": "catalog:",
     "steady-xml": "catalog:",
     "tinymce": "catalog:",
     "tyme4ts": "catalog:",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e317c5f..b02d913 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -647,6 +647,9 @@
       pinia:
         specifier: 'catalog:'
         version: 3.0.4(typescript@6.0.3)(vue@3.5.38(typescript@6.0.3))
+      sortablejs:
+        specifier: 'catalog:'
+        version: 1.15.7
       steady-xml:
         specifier: 'catalog:'
         version: 0.1.0
@@ -7724,8 +7727,8 @@
   orderedmap@2.1.1:
     resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==}
 
-  own-keys@1.0.1:
-    resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==}
+  own-keys@1.0.2:
+    resolution: {integrity: sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==}
     engines: {node: '>= 0.4'}
 
   oxfmt@0.56.0:
@@ -14218,7 +14221,7 @@
       object-inspect: 1.13.4
       object-keys: 1.1.1
       object.assign: 4.1.7
-      own-keys: 1.0.1
+      own-keys: 1.0.2
       regexp.prototype.flags: 1.5.4
       safe-array-concat: 1.1.4
       safe-push-apply: 1.0.0
@@ -16099,8 +16102,9 @@
 
   orderedmap@2.1.1: {}
 
-  own-keys@1.0.1:
+  own-keys@1.0.2:
     dependencies:
+      call-bound: 1.0.4
       get-intrinsic: 1.3.0
       object-keys: 1.1.1
       safe-push-apply: 1.0.0
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index ad49499..5df4a75 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -210,10 +210,10 @@
 allowBuilds:
   '@carbon/icons': true
   '@parcel/watcher': true
-  '@vue-office/docx': set this to true or false
-  '@vue-office/excel': set this to true or false
-  '@vue-office/pdf': set this to true or false
-  '@vue-office/pptx': set this to true or false
+  '@vue-office/docx': true
+  '@vue-office/excel': true
+  '@vue-office/pdf': true
+  '@vue-office/pptx': true
   core-js: true
   core-js-pure: true
   esbuild: true
diff --git a/src/views/mes/pro/workorder/data.ts b/src/views/mes/pro/workorder/data.ts
index fcac639..f277918 100644
--- a/src/views/mes/pro/workorder/data.ts
+++ b/src/views/mes/pro/workorder/data.ts
@@ -747,7 +747,6 @@
             field: 'drag',
             title: '',
             width: 40,
-            fixed: 'left',
             slots: { default: 'dragHandle' },
           } as const,
         ]
@@ -755,6 +754,7 @@
     {
       type: 'expand',
       width: 50,
+      slots: { content: 'expand_content' },
     } as const,
     {
       field: 'sort',
@@ -835,7 +835,7 @@
     },
     {
       title: '鎿嶄綔',
-      width: 120,
+      width: 200,
       fixed: 'right',
       slots: { default: 'actions' },
     },
diff --git a/src/views/mes/pro/workorder/modules/form.vue b/src/views/mes/pro/workorder/modules/form.vue
index dced20c..891a31e 100644
--- a/src/views/mes/pro/workorder/modules/form.vue
+++ b/src/views/mes/pro/workorder/modules/form.vue
@@ -332,7 +332,8 @@
 
 <template>
   <Modal :title="getTitle"
-         class="w-3/5">
+         class="w-3/5"
+         content-class="!max-h-[80vh] overflow-y-auto">
     <Form class="mx-4" />
     <!-- BOM / 宸ュ簭 / 鐗╂枡闇�姹� Tab -->
     <template v-if="showTabs">
@@ -350,7 +351,8 @@
           </div>
         </Tabs.TabPane>
         <Tabs.TabPane key="process"
-                      tab="宸ュ簭">
+                      tab="宸ュ簭"
+                      forceRender>
           <ProcessList
             ref="processListRef"
             :form-type="formType"
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>
diff --git a/vite.config.ts b/vite.config.ts
index 5a07335..dcf9f5b 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,14 +1,24 @@
+import { resolve } from "node:path";
 import { defineConfig } from "./internal/vite-config/src";
 
 export default defineConfig(async () => {
   return {
     application: {},
     vite: {
+      resolve: {
+        alias: {
+          '@vue-office/pdf': resolve(__dirname, 'node_modules/@vue-office/pdf/lib/v3/vue-office-pdf.mjs'),
+          '@vue-office/docx': resolve(__dirname, 'node_modules/@vue-office/docx/lib/v3/vue-office-docx.mjs'),
+          '@vue-office/excel': resolve(__dirname, 'node_modules/@vue-office/excel/lib/v3/vue-office-excel.mjs'),
+          '@vue-office/excel-css': resolve(__dirname, 'node_modules/@vue-office/excel/lib/v3/index.css'),
+          '@vue-office/pptx': resolve(__dirname, 'node_modules/@vue-office/pptx/lib/v3/vue-office-pptx.mjs'),
+        },
+      },
       oxc: {
         enable: false,
       },
       optimizeDeps: {
-        include: ["vue", "vue-router", "pinia", "ant-design-vue", "dayjs", "@vueuse/core", "axios"],
+        include: ["vue", "vue-router", "pinia", "ant-design-vue", "dayjs", "@vueuse/core", "axios", "@vue-office/pdf", "@vue-office/docx", "@vue-office/excel", "@vue-office/pptx"],
         exclude: ["dhtmlx-gantt", "tinymce", "bpmn-js", "bpmn-js-properties-panel", "video.js"],
       },
       build: {
@@ -28,6 +38,9 @@
                 if (["tinymce", "@tinymce/tinymce-vue", "@form-create/ant-design-vue"].some(dep => id.includes(dep))) {
                   return "vendor-editor";
                 }
+                if (["@vue-office"].some(dep => id.includes(dep))) {
+                  return "vendor-office-preview";
+                }
               }
             },
           },

--
Gitblit v1.9.3