Browse Source

feat(materials): support multi-select import and dedupe material options

Convert single-selected material handling into multi-select across the
order-form material picker and import flow.

- Change selectedMaterialId (string|null) to selectedMaterialIds (string[])
  in types and component state to allow multiple selections.
- Normalize API ids to strings when mapping material options to avoid type
  mismatches.
- Filter out options that are already imported into the materialDetails
  table (compare both itemId and id) so users don't see items they have
  imported.
- Update remote search and pagination mapping to apply the same dedupe
  filtering.
- Update handleImportSelectedMaterial to accept multiple selections,
  build an option map, filter out already-existing materials, prepare and
  calculate amounts for each selected item, emit a batch import event,
  and clear selections.

Why:
- Enable importing multiple materials at once to improve UX and speed.
- Prevent duplicate imports by filtering candidates using itemId/id.
- Ensure id comparisons are robust (stringified) to avoid missed matches.
yz 1 week ago
parent
commit
5ab340a1ef

+ 49 - 24
src/components/order-form/material-detail-mixin.js

@@ -136,10 +136,10 @@ export default {
       },
 
       /**
-       * 选中的物料ID - 当前在下拉框中选中的物料ID
-       * @type {string|null}
+       * 选中的物料ID列表 - 当前在下拉框中选中的物料ID(多选)
+       * @type {string[]}
        */
-      selectedMaterialId: null,
+      selectedMaterialIds: [],
 
       /**
        * 物料选项列表 - 远程搜索返回的物料选项
@@ -408,8 +408,8 @@ export default {
         if (response?.data?.success && response.data.data) {
           // 转换API返回的字段名称为组件所需的格式
           // getMaterialFullList返回的是SalesOrderItemListRecord[]数组
-          this.materialOptions = response.data.data.map(item => ({
-            id: item.id,
+          const mapped = response.data.data.map(item => ({
+            id: String(item.id),
             itemId: item.Item_ID,
             itemCode: item.Item_Code,
             itemName: item.Item_Name,
@@ -429,6 +429,13 @@ export default {
             // 保留原始数据以备后用
             _raw: item
           }))
+          // 过滤掉已导入到下方表格的数据(使用 itemId 或 id)
+          const existingItemIds = new Set((this.materialDetails || []).map(d => String(d.itemId)))
+          this.materialOptions = mapped.filter(opt => {
+            const itemIdStr = String(opt.itemId)
+            const idStr = String(opt.id)
+            return !(existingItemIds.has(itemIdStr) || existingItemIds.has(idStr))
+          })
         } else {
           this.materialOptions = []
           const errorMsg = response?.data?.msg || '搜索物料失败'
@@ -470,8 +477,8 @@ export default {
           const pageData = response.data.data
           const list = pageData.records || []
 
-          this.materialOptions = list.map(item => ({
-            id: item.id,
+          const mapped = list.map(item => ({
+            id: String(item.id),
             itemId: item.goodsId,
             itemCode: item.code,
             itemName: item.cname,
@@ -495,6 +502,13 @@ export default {
             // 保留原始数据以备后用
             _raw: item
           }))
+          // 过滤掉已导入到下方表格的数据(使用 itemId 或 id)
+          const existingItemIds = new Set((this.materialDetails || []).map(d => String(d.itemId)))
+          this.materialOptions = mapped.filter(opt => {
+            const itemIdStr = String(opt.itemId)
+            const idStr = String(opt.id)
+            return !(existingItemIds.has(itemIdStr) || existingItemIds.has(idStr))
+          })
         } else {
           this.materialOptions = []
           const errorMsg = response?.data?.msg || '搜索库存物料失败'
@@ -515,38 +529,49 @@ export default {
      * @this {MaterialDetailTableComponent}
      */
     handleImportSelectedMaterial() {
-      if (!this.selectedMaterialId) {
+      const ids = Array.isArray(this.selectedMaterialIds) ? this.selectedMaterialIds : []
+      if (!ids.length) {
         this.$message.warning('请先选择要导入的物料')
         return
       }
 
-      // 查找选中的物料数据
-      const selectedMaterial = this.materialOptions.find(item => item.id === this.selectedMaterialId)
-      if (!selectedMaterial) {
+      // 构建选项映射,便于根据ID快速查找
+      const optionMap = new Map(this.materialOptions.map(opt => [String(opt.id), opt]))
+      const selectedOptions = ids.map(id => optionMap.get(String(id))).filter(Boolean)
+
+      if (!selectedOptions.length) {
         this.$message.warning('未找到选中的物料数据')
         return
       }
 
-      // 检查是否已存在相同物料
-      const existingMaterial = this.materialDetails.find(item => item.itemCode === selectedMaterial.itemCode)
-      if (existingMaterial) {
-        this.$message.warning(`物料 ${selectedMaterial.itemName} 已存在,请勿重复导入`)
+      // 已存在的物料(按 itemId 去重,兼容按 id 判重)
+      const existingSet = new Set((this.materialDetails || []).map(item => String(item.itemId)))
+
+      const toImport = selectedOptions.filter(opt => {
+        const byItemId = existingSet.has(String(opt.itemId))
+        const byId = existingSet.has(String(opt.id))
+        return !(byItemId || byId)
+      })
+
+      if (!toImport.length) {
+        this.$message.warning('选择的物料均已存在,请勿重复导入')
         return
       }
 
-      // 构造物料明细数据
-      let materialDetail = this.prepareMaterialDetailData(selectedMaterial)
-
-      // 导入时自动计算金额
-      materialDetail = this.calculateAmounts(materialDetail)
+      const materialDetails = toImport.map(material => {
+        let detail = this.prepareMaterialDetailData(material)
+        detail = this.calculateAmounts(detail)
+        return detail
+      })
 
       // 触发导入事件
-      this.$emit(MATERIAL_DETAIL_EVENTS.MATERIAL_IMPORT, [materialDetail])
+      this.$emit(MATERIAL_DETAIL_EVENTS.MATERIAL_IMPORT, materialDetails)
       this.$emit(MATERIAL_DETAIL_EVENTS.REFRESH)
 
-      // 清空选择
-      this.selectedMaterialId = null
-      this.materialOptions = []
+      // 清空选择,并从候选项中移除已导入的项
+      this.selectedMaterialIds = []
+      const importedIdSet = new Set(ids.map(id => String(id)))
+      this.materialOptions = this.materialOptions.filter(opt => !importedIdSet.has(String(opt.id)))
     },
 
     /**

+ 4 - 3
src/components/order-form/material-detail-table.vue

@@ -18,10 +18,11 @@
         <!-- 物料选择区域 -->
         <div class="material-select-container">
           <el-select
-            v-model="selectedMaterialId"
-            placeholder="请选择物料"
+            v-model="selectedMaterialIds"
+            placeholder="请选择物料(可多选)"
             filterable
             remote
+            multiple
             reserve-keyword
             :remote-method="remoteSearchMaterial"
             :loading="materialLoading"
@@ -43,7 +44,7 @@
             type="primary"
             icon="el-icon-plus"
             size="small"
-            :disabled="!selectedMaterialId"
+            :disabled="!selectedMaterialIds || selectedMaterialIds.length === 0"
             @click="handleImportSelectedMaterial"
           >
             增加

+ 1 - 1
src/components/order-form/types.d.ts

@@ -643,7 +643,7 @@ export interface MaterialDetailTableComponent extends Vue {
   // data
   formData: Partial<MaterialDetailRecord>;
   page: PageOption;
-  selectedMaterialId: string | null;
+  selectedMaterialIds: string[];
   materialOptions: MaterialOption[];
   materialLoading: boolean;
   searchTimer: number | null;