From 14817696a26eb142ccae03fa7c43050840e057f5 Mon Sep 17 00:00:00 2001
From: hsy <1029150275@qq.com>
Date: 星期一, 17 八月 2026 09:35:03 +0800
Subject: [PATCH] feat(mes): 优化生产订单与排产任务列表显示,新增启停工及校验逻辑 1. 生产订单列表展示优化 (WorkOrder)   - 新增「生产状态」列,全局注册 `ORDER_PRODUCTION_STATUS` 常量并绑定系统动态字典。   - 彻底重构「工序生产进度」单元格显示:弃用臃肿的 Steps 组件,改为极简横向布局(彩色状态圆点 + 进度百分比),解决表格行高被强行撑开及内容截断问题,支持横向滚动。   - 优化「完成进度」列宽及展示样式,将百分比数值外置同行显示,更加直观。 2. 排产任务列表改造 (Task)   - 顶部表头重构,由单一标题重构为「全部 / 未完成 / 已完成」的 Tabs 标签页切换结构。   - 重写数据过滤逻辑,剥离写死的 status 查询,改为使用 `scheduleStatus` 字段动态响应 Tabs 切换 (0: 未完成, 1: 已完成)。 3. 工单业务逻辑增强与状态流转   - 落实「工单查询逻辑设计方案」,优化底层的工单条件检索逻辑。   - 实现「添加工单启停功能」与「停工状态」,支持工单在执行过程中的暂停与恢复业务流转。   - 强化容错与防呆设计:操作「完成订单」时增加前置校验 `handleCheckFinish`,若生产状态为未完成,强制弹出二次确认 Modal,避免误操作。 # 相关讨论/参考方案 # - 添加工单启停功能 # - 停工状态功能设计方案 # - 工单查询逻辑设计方案 # - 工单完成进度显示优化

---
 src/views/mes/pro/workorder/modules/process-list.vue |  206 +++++++++++++++++++++++++++++++++++++++++----------
 1 files changed, 165 insertions(+), 41 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..283424d 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;
   }
 }
 
@@ -139,9 +156,8 @@
           keyFlag: item.keyFlag,
           checkFlag: item.checkFlag,
           backflushFlag: item.backflushFlag,
-          outputItemId: item.outputItemId,
-          outputItemCode: item.outputItemCode,
-          outputItemName: item.outputItemName,
+          outputItemIds: item.outputItemIds,
+          outputItemNames: item.outputItemNames,
           remark: item.remark,
           bomItems: item.bomItems,
         }));
@@ -230,6 +246,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 +339,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 +399,11 @@
         <TableAction
           :actions="[
             {
+              label: '鏂板鐗╂枡',
+              type: 'link',
+              onClick: handleMaterialInput.bind(null, row),
+            },
+            {
               label: $t('common.edit'),
               type: 'link',
               ifShow: isEditable,
@@ -300,7 +422,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 +432,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 +444,4 @@
       </template>
     </Grid>
   </div>
-</template>
\ No newline at end of file
+</template>

--
Gitblit v1.9.3