银川
1.生产订单新增修改时候工序列表拖拽问题解决
2.生产订单新增修改时候工序可以新增物料并更新
已修改7个文件
227 ■■■■ 文件已修改
package.json 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pnpm-lock.yaml 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pnpm-workspace.yaml 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/workorder/data.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/workorder/modules/form.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/workorder/modules/process-list.vue 181 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite.config.ts 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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:",
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
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
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' },
    },
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"
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,
    {
  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: ({ newDraggableIndex, oldDraggableIndex }) => {
    onEnd: (evt) => {
      const { oldIndex, newIndex, item } = evt;
        if (
          newDraggableIndex !== undefined &&
          oldDraggableIndex !== undefined &&
          oldDraggableIndex !== newDraggableIndex
        oldIndex === undefined ||
        newIndex === undefined ||
        oldIndex === newIndex
        ) {
          // 重新排序数组
          list.value.splice(
            newDraggableIndex,
            0,
            list.value.splice(oldDraggableIndex, 1)[0]!,
          );
        return;
      }
      // 还原 DOM:Sortable 已移动节点,需放回原位,由数据驱动重渲染
      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((item, index) => {
            item.sort = index + 1;
      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>
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";
                }
              }
            },
          },