已修改8个文件
已删除1个文件
348 ■■■■■ 文件已修改
package.json 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pnpm-lock.yaml 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pnpm-workspace.yaml 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/order/modules/form.vue 48 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/erp/purchase/order/modules/process-select-modal.vue 53 ●●●●● 补丁 | 查看 | 原始文档 | 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 201 ●●●● 补丁 | 查看 | 原始文档 | 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/erp/purchase/order/modules/form.vue
@@ -1,7 +1,7 @@
<script lang="ts" setup>
import type { FormType } from '../data';
import type { ErpPurchaseOrderApi, ProcessDefinition } from '#/api/erp/purchase/order';
import type { ErpPurchaseOrderApi } from '#/api/erp/purchase/order';
import { computed, ref } from 'vue';
@@ -14,7 +14,6 @@
import {
  confirmReceipt,
  createPurchaseOrder,
  getPurchaseApproveProcessList,
  getPurchaseOrder,
  updatePurchaseOrder,
} from '#/api/erp/purchase/order';
@@ -22,13 +21,11 @@
import { useFormSchema } from '../data';
import PurchaseOrderItemForm from './item-form.vue';
import ProcessSelectModal from './process-select-modal.vue';
const emit = defineEmits(['success']);
const formData = ref<ErpPurchaseOrderApi.PurchaseOrder>();
const formType = ref<FormType>('create');
const itemFormRef = ref<InstanceType<typeof PurchaseOrderItemForm>>();
const selectedProcessId = ref<string>('');
const getTitle = computed(() => {
  if (formType.value === 'create') {
@@ -58,11 +55,6 @@
  },
});
const [ProcessModal, processModalApi] = useVbenModal({
  connectedComponent: ProcessSelectModal,
  destroyOnClose: true,
});
/** æ›´æ–°é‡‡è´­è®¢å•项 */
function handleUpdateItems(items: ErpPurchaseOrderApi.PurchaseOrderItem[]) {
  formData.value = modalApi.getData<ErpPurchaseOrderApi.PurchaseOrder>();
@@ -86,12 +78,6 @@
  });
}
/** æµç¨‹é€‰æ‹©æˆåŠŸ */
function handleProcessSelectSuccess(processId: string) {
  selectedProcessId.value = processId;
  submitOrder();
}
/** æäº¤è®¢å• */
async function submitOrder() {
  const data = (await formApi.getValues()) as ErpPurchaseOrderApi.PurchaseOrder;
@@ -101,9 +87,6 @@
  }));
  if (data.fileUrl && Array.isArray(data.fileUrl)) {
    data.fileUrl = data.fileUrl.length > 0 ? data.fileUrl[0] : '';
  }
  if (selectedProcessId.value) {
    data.processDefinitionId = selectedProcessId.value;
  }
  await (formType.value === 'create'
    ? createPurchaseOrder(data)
@@ -145,35 +128,15 @@
    }
    modalApi.lock();
    if (formType.value === 'create') {
      try {
        const processes = await getPurchaseApproveProcessList();
        if (processes.length === 1) {
          selectedProcessId.value = processes[0].id;
          await submitOrder();
        } else if (processes.length > 1) {
          processModalApi.setData(processes).open();
        } else {
          message.error('采购审核分类下没有可用的流程模型,请先创建并发布流程');
        }
      } catch (error: any) {
        message.error(error.message || '获取流程列表失败');
      } finally {
        modalApi.unlock();
      }
    } else {
      try {
        await submitOrder();
      } finally {
        modalApi.unlock();
      }
    try {
      await submitOrder();
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      formData.value = undefined;
      selectedProcessId.value = '';
      return;
    }
    const data = modalApi.getData<{ formType: FormType; id?: number }>();
@@ -205,7 +168,6 @@
    class="w-3/4"
    :show-confirm-button="formType !== 'detail'"
  >
    <ProcessModal @success="handleProcessSelectSuccess" />
    <Form class="mx-3">
      <template #items>
        <PurchaseOrderItemForm
src/views/erp/purchase/order/modules/process-select-modal.vue
ÎļþÒÑɾ³ý
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,
    {
      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: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((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>
</template>
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";
                }
              }
            },
          },