From 5d321660a2f1a4b8756f816283e96481a8c934e4 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期三, 28 十月 2026 14:26:30 +0800
Subject: [PATCH] feat(app): 更新应用名称并优化批次绑定功能

---
 src/views/mes/pro/batch/modules/form.vue |  322 ++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 files changed, 314 insertions(+), 8 deletions(-)

diff --git a/src/views/mes/pro/batch/modules/form.vue b/src/views/mes/pro/batch/modules/form.vue
index 2e45243..1ef766d 100644
--- a/src/views/mes/pro/batch/modules/form.vue
+++ b/src/views/mes/pro/batch/modules/form.vue
@@ -1,11 +1,11 @@
 <script lang="ts" setup>
-  import type { FormType } from "../data";
+  import type { FormType, PalletOption, TonBagOption } from "../data";
 
   import type { VxeTableGridOptions } from "#/adapter/vxe-table";
   import type { MesProBatchApi } from "#/api/mes/pro/batch";
   import type { SystemUserApi } from "#/api/system/user";
 
-  import { computed, nextTick, ref } from "vue";
+  import { computed, nextTick, reactive, ref } from "vue";
 
   import { useVbenModal } from "@vben/common-ui";
   import { DICT_TYPE } from "@vben/constants";
@@ -15,11 +15,20 @@
 
   import { useVbenForm } from "#/adapter/form";
   import { TableAction, useVbenVxeGrid } from "#/adapter/vxe-table";
-  import { createBatch, getBatch, updateBatch } from "#/api/mes/pro/batch";
+  import {
+    createBatch,
+    fillBatchInfo,
+    getBatch,
+    updateBatch,
+  } from "#/api/mes/pro/batch";
+  import { getUnboundPalletPage } from "#/api/mes/pro/pallet";
+  import { getUnboundTonBagPage } from "#/api/mes/pro/bagCode";
   import { $t } from "#/locales";
   import { UserSelect } from "#/views/system/user/components";
 
   import {
+    applyPalletOptionState,
+    applyTonBagOptionState,
     useFormSchema,
     useWorkerEditGridColumns,
     useWorkerReadGridColumns,
@@ -31,9 +40,273 @@
 
   const formType = ref<FormType>("create");
   const formData = ref<MesProBatchApi.Batch>();
+  /** 缁戝畾涓嬫媺姣忔鍔犺浇鐨勬潯鏁� */
+  const BINDING_PAGE_SIZE = 50;
+  /** 璺濈涓嬫媺搴曢儴澶氬皯鍍忕礌鏃惰Е鍙戝姞杞戒笅涓�椤� */
+  const BINDING_SCROLL_THRESHOLD = 32;
+  /** 杈撳叆妗嗘悳绱㈤槻鎶栨椂闂达紙姣锛� */
+  const BINDING_SEARCH_DEBOUNCE = 300;
+
+  /**
+   * 缁戝畾涓嬫媺鐨勫垎椤靛姞杞界姸鎬併��
+   *
+   * 涓嬫媺閫夐」鏈韩灏辨槸娓叉煋鏁版嵁婧愶紙淇濇寔鏁扮粍寮曠敤涓嶅彉锛岀洿鎺� splice 鏇存柊锛夛紝
+   * 宸插姞杞界殑鍒嗛〉缁撴灉鏀惧湪 loaded 涓紝璺ㄩ〉閫変腑鐨勯�夐」鍗曠嫭璁板綍锛岄伩鍏嶆崲椤垫悳绱㈠悗鏍囩涓㈠け銆�
+   */
+  interface BindingLoader<T extends PalletOption> {
+    hasMore: boolean;
+    keyword: string;
+    loaded: T[];
+    loading: boolean;
+    pageNo: number;
+    seq: number;
+    timer?: ReturnType<typeof setTimeout>;
+  }
+
+  function createBindingLoader<T extends PalletOption>(): BindingLoader<T> {
+    return {
+      hasMore: true,
+      keyword: "",
+      loaded: [],
+      loading: false,
+      pageNo: 0,
+      seq: 0,
+    };
+  }
+
+  const palletOptions = reactive<PalletOption[]>([]);
+  const tonBagOptions = reactive<TonBagOption[]>([]);
+  const palletSelected = new Map<number, PalletOption>();
+  const tonBagSelected = new Map<number, TonBagOption>();
+  const palletLoader = createBindingLoader<PalletOption>();
+  const tonBagLoader = createBindingLoader<TonBagOption>();
+
+  /** 宸插姞杞藉垎椤电粨鏋� + 璺ㄩ〉閫変腑椤瑰悎骞朵负涓嬫媺娓叉煋鏁版嵁 */
+  function renderBindingOptions<T extends PalletOption>(
+    target: T[],
+    loader: BindingLoader<T>,
+    selected: Map<number, T>
+  ) {
+    const merged = [...loader.loaded];
+    const loadedIds = new Set(loader.loaded.map((option) => option.value));
+    selected.forEach((option, id) => {
+      if (!loadedIds.has(id)) merged.push(option);
+    });
+    target.splice(0, target.length, ...merged);
+  }
+
+  /** 鍚屾閫変腑椤癸紙鎹㈤〉鎼滅储鍚庝粛闇�灞曠ず宸查�夋爣绛撅級 */
+  function syncBindingSelection<T extends PalletOption>(
+    target: T[],
+    loader: BindingLoader<T>,
+    selected: Map<number, T>,
+    ids: number[]
+  ) {
+    const previous = new Map(selected);
+    selected.clear();
+    ids.forEach((id) => {
+      const option = previous.get(id)
+        ?? loader.loaded.find((item) => item.value === id);
+      if (option) selected.set(id, option);
+    });
+    renderBindingOptions(target, loader, selected);
+  }
+
+  /** 閫夐」绂佺敤鎬佷緷璧栧綋鍓嶈〃鍗曞�硷紝姣忔鍔犺浇鏂颁竴椤靛悗閲嶆柊璁$畻 */
+  async function refreshBindingOptionState() {
+    const values = await formApi.getValues();
+    const existingBinding = formData.value?.bindingMode;
+    applyPalletOptionState(palletOptions, values, existingBinding);
+    applyTonBagOptionState(tonBagOptions, values, existingBinding);
+  }
+
+  /** 鎷夊彇涓�椤电粦瀹氬�欓�夐」锛況eset=true 琛ㄧず浠庣涓�椤甸噸鏂板紑濮嬶紙棣栨鎵撳紑銆佹悳绱級 */
+  async function loadBindingPage<T extends PalletOption>(
+    loader: BindingLoader<T>,
+    target: T[],
+    selected: Map<number, T>,
+    fetchPage: (params: {
+      keyword?: string;
+      pageNo: number;
+      pageSize: number;
+    }) => Promise<{ list: T[]; total: number }>,
+    reset: boolean
+  ) {
+    if (!reset && (loader.loading || !loader.hasMore)) return;
+    const seq = ++loader.seq;
+    loader.loading = true;
+    try {
+      const pageNo = reset ? 1 : loader.pageNo + 1;
+      const page = await fetchPage({
+        keyword: loader.keyword || undefined,
+        pageNo,
+        pageSize: BINDING_PAGE_SIZE,
+      });
+      // 宸茶鏇存柊鐨勬悳绱㈡垨骞跺彂璇锋眰瑕嗙洊锛屼涪寮冩湰娆$粨鏋�
+      if (seq !== loader.seq) return;
+      loader.loaded = reset ? page.list : [...loader.loaded, ...page.list];
+      loader.pageNo = pageNo;
+      loader.hasMore = loader.loaded.length < page.total;
+      renderBindingOptions(target, loader, selected);
+      await refreshBindingOptionState();
+    } finally {
+      if (seq === loader.seq) loader.loading = false;
+    }
+  }
+
+  function fetchPalletPage(params: {
+    keyword?: string;
+    pageNo: number;
+    pageSize: number;
+  }) {
+    return getUnboundPalletPage(params).then((page) => ({
+      list: page.list.map((pallet) => ({
+        label: `${pallet.tempCode ?? `鎵樼洏${pallet.id}`} 路 ${pallet.itemName || "寰呭畬鍠勫搧绉�"}锛堝凡瑁� ${pallet.bagCount ?? 0} 琚嬶級`,
+        value: pallet.id as number,
+        itemId: pallet.itemId,
+        itemName: pallet.itemName,
+        itemCode: pallet.itemCode,
+      })),
+      total: page.total,
+    }));
+  }
+
+  function fetchTonBagPage(params: {
+    keyword?: string;
+    pageNo: number;
+    pageSize: number;
+  }) {
+    return getUnboundTonBagPage(params).then((page) => ({
+      list: page.list.map((bag) => ({
+        label: `${bag.code ?? `鍚ㄨ${bag.id}`} 路 ${bag.itemName || "寰呭畬鍠勫搧绉�"}`,
+        value: bag.id as number,
+        itemId: bag.itemId,
+        itemName: bag.itemName,
+        itemCode: bag.itemCode,
+      })),
+      total: page.total,
+    }));
+  }
+
+  function loadPalletPage(reset: boolean) {
+    return loadBindingPage(palletLoader, palletOptions, palletSelected, fetchPalletPage, reset);
+  }
+
+  function loadTonBagPage(reset: boolean) {
+    return loadBindingPage(tonBagLoader, tonBagOptions, tonBagSelected, fetchTonBagPage, reset);
+  }
+
+  /** 閲嶇疆涓嬫媺鍒嗛〉鐘舵�侊紙姣忔鎵撳紑寮圭獥璋冪敤锛夛紝鍚屾椂璁╁湪閫旇姹傜殑缁撴灉澶辨晥 */
+  function resetBindingLoader<T extends PalletOption>(
+    loader: BindingLoader<T>,
+    target: T[],
+    selected: Map<number, T>
+  ) {
+    clearTimeout(loader.timer);
+    loader.seq += 1;
+    loader.hasMore = true;
+    loader.keyword = "";
+    loader.loaded = [];
+    loader.loading = false;
+    loader.pageNo = 0;
+    target.splice(0, target.length);
+    selected.clear();
+  }
+
+  function isPopupNearBottom(event: Event) {
+    const element = event.target as HTMLElement;
+    return element.scrollHeight - element.scrollTop - element.clientHeight <= BINDING_SCROLL_THRESHOLD;
+  }
+
+  function scheduleBindingSearch<T extends PalletOption>(
+    loader: BindingLoader<T>,
+    keyword: string,
+    reload: (reset: boolean) => Promise<void>
+  ) {
+    loader.keyword = keyword.trim();
+    clearTimeout(loader.timer);
+    loader.timer = setTimeout(() => void reload(true), BINDING_SEARCH_DEBOUNCE);
+  }
+
+  function handlePalletSearch(keyword: string) {
+    scheduleBindingSearch(palletLoader, keyword, loadPalletPage);
+  }
+
+  function handleTonBagSearch(keyword: string) {
+    scheduleBindingSearch(tonBagLoader, keyword, loadTonBagPage);
+  }
+
+  function handlePalletPopupScroll(event: Event) {
+    if (isPopupNearBottom(event)) void loadPalletPage(false);
+  }
+
+  function handleTonBagPopupScroll(event: Event) {
+    if (isPopupNearBottom(event)) void loadTonBagPage(false);
+  }
+
+  const palletHandlers = {
+    onChange: handlePalletChange,
+    onPopupScroll: handlePalletPopupScroll,
+    onSearch: handlePalletSearch,
+  };
+  const tonBagHandlers = {
+    onChange: handleTonBagChange,
+    onPopupScroll: handleTonBagPopupScroll,
+    onSearch: handleTonBagSearch,
+  };
+
+  async function handlePalletChange(ids: number[] = []) {
+    const values = await formApi.getValues();
+    if (ids.length && (values.tonBagIds?.length || formData.value?.bindingMode === 'tonBag')) {
+      message.warning('缁戝畾鎵樼洏鍜岀粦瀹氬惃琚嬪彧鑳介�夋嫨涓�绉�');
+      await formApi.setFieldValue('palletIds', []);
+      syncBindingSelection(palletOptions, palletLoader, palletSelected, []);
+      return;
+    }
+    const selected = ids
+      .map((id) => palletSelected.get(id) ?? palletLoader.loaded.find((option) => option.value === id))
+      .filter((option): option is PalletOption => !!option);
+    const source = formData.value?.itemId ? formData.value : selected[0];
+    let finalIds = ids;
+    if (selected.some(option => !option.itemId || (source?.itemId && option.itemId !== source.itemId))) {
+      message.warning('鐢熶骇鎵规鍙兘缁戝畾鐩稿悓鍝佺鐨勬墭鐩�');
+      finalIds = selected.filter(option => option.itemId === source?.itemId).map(option => option.value);
+      await formApi.setFieldValue('palletIds', finalIds);
+    }
+    syncBindingSelection(palletOptions, palletLoader, palletSelected, finalIds);
+    await refreshBindingOptionState();
+    await formApi.setFieldValue('itemId', source?.itemId);
+    await formApi.setFieldValue('itemName', source?.itemName);
+    await formApi.setFieldValue('itemCode', source?.itemCode);
+  }
+
+  async function handleTonBagChange(ids: number[] = []) {
+    const values = await formApi.getValues();
+    if (ids.length && (values.palletIds?.length || formData.value?.bindingMode === 'pallet')) {
+      message.warning('缁戝畾鎵樼洏鍜岀粦瀹氬惃琚嬪彧鑳介�夋嫨涓�绉�');
+      await formApi.setFieldValue('tonBagIds', []);
+      syncBindingSelection(tonBagOptions, tonBagLoader, tonBagSelected, []);
+      return;
+    }
+    const selected = ids
+      .map((id) => tonBagSelected.get(id) ?? tonBagLoader.loaded.find((option) => option.value === id))
+      .filter((option): option is TonBagOption => !!option);
+    const source = formData.value?.itemId ? formData.value : selected[0];
+    let finalIds = ids;
+    if (selected.some(option => option.itemId !== source?.itemId)) {
+      message.warning('鐢熶骇鎵规鍙兘缁戝畾鐩稿悓鍝佺鐨勫惃琚�');
+      finalIds = selected.filter(option => option.itemId === source?.itemId).map(option => option.value);
+      await formApi.setFieldValue('tonBagIds', finalIds);
+    }
+    syncBindingSelection(tonBagOptions, tonBagLoader, tonBagSelected, finalIds);
+    await refreshBindingOptionState();
+    await formApi.setFieldValue('itemId', source?.itemId);
+    await formApi.setFieldValue('itemName', source?.itemName);
+    await formApi.setFieldValue('itemCode', source?.itemCode);
+  }
 
   const isEditable = computed(() =>
-    ["create", "update"].includes(formType.value)
+    ["create", "update", "fill"].includes(formType.value)
   );
 
   const getTitle = computed(() => {
@@ -43,6 +316,9 @@
       }
       case "update": {
         return $t("ui.actionTitle.edit", ["鐢熶骇鎵规"]);
+      }
+      case "fill": {
+        return "琛ュ綍鎵规淇℃伅";
       }
       default: {
         return $t("ui.actionTitle.create", ["鐢熶骇鎵规"]);
@@ -82,8 +358,9 @@
   let workerRowSeq = 0;
 
   /** 娣诲姞浜哄憳琛� */
-  function handleAddWorker() {
-    workerGridApi.grid.insertAt({ _rowKey: -(++workerRowSeq) }, -1);
+  async function handleAddWorker() {
+    await waitWorkerGridReady();
+    await workerGridApi.grid.insertAt({ _rowKey: -(++workerRowSeq) }, -1);
   }
 
   /** 鍒犻櫎浜哄憳琛� */
@@ -177,7 +454,13 @@
       modalApi.lock();
       try {
         const data = (await formApi.getValues()) as MesProBatchApi.Batch;
-        if (formData.value?.id) {
+        if (data.palletIds?.length && data.tonBagIds?.length) {
+          message.warning('缁戝畾鎵樼洏鍜岀粦瀹氬惃琚嬪彧鑳介�夋嫨涓�绉�');
+          return;
+        }
+        if (formType.value === "fill") {
+          await fillBatchInfo({ ...data, id: formData.value!.id, workers });
+        } else if (formData.value?.id) {
           await updateBatch({ ...data, id: formData.value.id, workers });
         } else {
           await createBatch({ ...data, workers });
@@ -196,7 +479,25 @@
       }
       const data = modalApi.getData<{ formType: FormType; id?: number }>();
       formType.value = data.formType;
-      formApi.setState({ schema: useFormSchema(formType.value) });
+      // 姣忔鎵撳紑閲嶇疆缁戝畾鍊欓�夛紝涓嬫媺鏀逛负婊氬姩鍒嗛〉鍔犺浇锛岄鎵瑰湪琛ㄥ崟鎸傝浇鍚庡紓姝ユ媺鍙�
+      resetBindingLoader(palletLoader, palletOptions, palletSelected);
+      resetBindingLoader(tonBagLoader, tonBagOptions, tonBagSelected);
+      formApi.setState({
+        schema: useFormSchema(
+          formType.value,
+          palletOptions,
+          palletHandlers,
+          tonBagOptions,
+          tonBagHandlers
+        ),
+      });
+      if (formType.value !== "detail") {
+        // 娓呯┖涓婃鎵撳紑鏃跺嬀閫夌殑鎵樼洏/鍚ㄨ锛岄伩鍏嶆畫鐣欏�艰瑙﹀彂銆屾壒娆¤繘鍏ョ敓浜т腑銆�
+        await formApi.setFieldValue("palletIds", undefined);
+        await formApi.setFieldValue("tonBagIds", undefined);
+        void loadPalletPage(true);
+        void loadTonBagPage(true);
+      }
       // 鏍规嵁琛ㄥ崟鎬佸垏鎹汉鍛樺瓙琛ㄥ垪
       workerGridApi.setGridOptions({
         columns: isEditable.value
@@ -207,7 +508,11 @@
         modalApi.lock();
         try {
           formData.value = await getBatch(data.id);
+          const existingBinding = formData.value.bindingMode;
+          formApi.setState({ schema: useFormSchema(formType.value, palletOptions, palletHandlers, tonBagOptions, tonBagHandlers, existingBinding) });
           await formApi.setValues(formData.value);
+          // 宸叉湁缁戝畾浼氶攣瀹氬彟涓�渚т笅鎷夛紝閫夐」绂佺敤鎬侀渶瑕佹寜鏈�鏂拌〃鍗曞�奸噸绠�
+          await refreshBindingOptionState();
           const workers = (formData.value.workers ?? []).map(
             (worker, index) => ({ ...worker, _rowKey: worker.id ?? index })
           );
@@ -219,6 +524,7 @@
       } else {
         await waitWorkerGridReady();
         await workerGridApi.grid.loadData([]);
+        await handleAddWorker();
       }
     },
   });

--
Gitblit v1.9.3