Przeglądaj źródła

Merge branch 'ecp' of http://git.echepei.com/gubersail/gubersail-platform-ui into ecp

Qukatie 6 dni temu
rodzic
commit
ff4df6acc0

+ 325 - 26
src/components/forecast-form/forecast-form-mixin.js

@@ -284,8 +284,22 @@ export default {
        */
       selectedStockId: null,
 
+      // 选择状态:存储已选中的行唯一键(跨分页)
+      /** @type {Array<string|number>} */
+      selectedRowKeys: [],
+
+      // 程序化同步选择的守卫标记,避免回调环
+      /** @type {boolean} */
+      selectionSyncing: false,
+
       /** 当前库存 */
-      currentInventory: null
+      currentInventory: null,
+
+      // 分页状态
+      /** 当前页(从1开始) */
+      currentPage: 1,
+      /** 每页条数(默认10) */
+      pageSize: 10
     }
   },
 
@@ -305,6 +319,33 @@ export default {
         return this.title
       }
       return this.isEdit ? '编辑销售预测' : '新增销售预测'
+    },
+
+    /**
+     * 物料总数(用于分页 total)
+     * @returns {number}
+     */
+    total() {
+      return Array.isArray(this.stockTableData) ? this.stockTableData.length : 0
+    },
+
+    /**
+     * 当前页数据(前端分页)
+     * @returns {Array<import('@/api/types/order').PjpfStockDesc & { forecastQuantity: number, brandCode?: string, storeInventory?: string }>}
+     */
+    pagedStockTableData() {
+      const list = Array.isArray(this.stockTableData) ? this.stockTableData : []
+      const size = Number(this.pageSize) > 0 ? Number(this.pageSize) : 10
+      const page = Number(this.currentPage) > 0 ? Number(this.currentPage) : 1
+      const start = (page - 1) * size
+      const end = start + size
+      return list.slice(start, end)
+    },
+
+    // 是否有选中项(用于禁用批量删除按钮)
+    /** @returns {boolean} */
+    hasSelection() {
+      return Array.isArray(this.selectedRowKeys) && this.selectedRowKeys.length > 0
     }
   },
 
@@ -353,6 +394,15 @@ export default {
               this.checkForecastByMonthAndEmit && this.checkForecastByMonthAndEmit()
             }
           })
+        } else {
+          // 弹窗关闭:编辑态下清空选择,防止跨会话污染
+          if (this.isEdit) {
+            this.selectedRowKeys = []
+            this.selectedStockId = null
+            this.$nextTick(() => {
+              this.syncTableSelection && this.syncTableSelection()
+            })
+          }
         }
       },
       immediate: true
@@ -444,6 +494,8 @@ export default {
         if (this.visible && !this.isEdit) {
           this.checkForecastByMonthAndEmit && this.checkForecastByMonthAndEmit()
         }
+        // 年份变更重置分页到第一页
+        this.currentPage = 1
       }
     },
     'formData.month': {
@@ -451,6 +503,8 @@ export default {
         if (this.visible && !this.isEdit) {
           this.checkForecastByMonthAndEmit && this.checkForecastByMonthAndEmit()
         }
+        // 月份变更重置分页到第一页
+        this.currentPage = 1
       }
     },
 
@@ -623,7 +677,15 @@ export default {
                 forecastQuantity: Number(item.forecastQuantity || 0)
               }))
               // 合并接口库存数据以支持回显
-              this.mergeEchoStoreInventory && this.mergeEchoStoreInventory().catch(() => {})
+              try {
+                if (this.mergeEchoStoreInventory) {
+                  await this.mergeEchoStoreInventory()
+                }
+              } catch (e) {
+                console.warn('合并库存回显失败:', e)
+              }
+              // 合并完成后,规范化分页并回显选择(首屏强制回显)
+              this.normalizePageAfterMutations()
             } catch (e) {
               console.warn('映射详情明细失败:', e)
             }
@@ -1169,6 +1231,8 @@ export default {
         this.stockDescList = []
         this.stockSelectOptions = []
         this.selectedStockId = null
+        // 重置选择状态
+        this.selectedRowKeys = []
         const res = await getUserLinkGoods()
         const payload = res && res.data && res.data.data ? res.data.data : null
         const brandList = (payload && payload.pjpfBrandDescList) || []
@@ -1176,13 +1240,12 @@ export default {
         this.brandDescList = brandList
         // 存储库存列表供选择用,不直接展示到表格
         this.stockDescList = stockList
-        // 构造下拉选项,label 使用 cname,value 使用 id
-        this.stockSelectOptions = stockList.map(item => ({
-          label: item.cname,
-          value: item.id
-        }))
         // 默认显示全部物料至下方表格,预测数量默认 0,用户可手动删除不需要的物料
         this.stockTableData = stockList.map(item => ({ ...item, forecastQuantity: 0 }))
+        // 根据表格中已有的物料,过滤下拉选项
+        this.updateStockSelectOptions()
+        // 规范化分页并回显选择(新增模式首次加载)
+        this.normalizePageAfterMutations()
       } catch (e) {
         console.error('加载用户关联商品失败:', e)
         this.$message.error(e.message || '加载用户关联商品失败')
@@ -1204,7 +1267,7 @@ export default {
         return
       }
       // 查找明细
-      const stock = this.stockDescList.find(s => s.id === this.selectedStockId)
+      const stock = this.stockDescList.find(s => String(s.id) === this.selectedStockId)
       if (!stock) {
         this.$message.error('未找到所选物料数据,请重新选择')
         return
@@ -1213,15 +1276,15 @@ export default {
       // 防止重复导入 - 使用多个字段进行更全面的重复检查
       const exists = this.stockTableData.some(row => {
         // 优先使用 id 进行匹配
-        if (row.id && stock.id && row.id === stock.id) {
+        if (row.id !== undefined && row.id !== null && stock.id !== undefined && stock.id !== null && String(row.id) === String(stock.id)) {
           return true
         }
         // 使用 goodsId 进行匹配
-        if (row.goodsId && stock.goodsId && row.goodsId === stock.goodsId) {
+        if (row.goodsId !== undefined && row.goodsId !== null && stock.goodsId !== undefined && stock.goodsId !== null && String(row.goodsId) === String(stock.goodsId)) {
           return true
         }
         // 使用 code 进行匹配
-        if (row.code && stock.code && row.code === stock.code) {
+        if (row.code && stock.code && String(row.code) === String(stock.code)) {
           return true
         }
         return false
@@ -1237,43 +1300,62 @@ export default {
       this.stockTableData.push({ ...stock, forecastQuantity: 0 })
       // 清空已选
       this.selectedStockId = null
+      // 导入后更新下拉选项(过滤掉已在表格中的物料)
+      this.updateStockSelectOptions()
+
+      // 导入后保持当前页不变;规范化分页以应对边界,并同步选择回显
+      this.normalizePageAfterMutations()
     },
 
     /**
-     * 删除物料行
-     * @description 在下方物料表格中删除指定行,包含二次确认流程;删除后保持数据与UI同步。
-     * @param {import('./types').ForecastFormMixinData['stockTableData'][number]} row - 待删除的表格行数据
-     * @param {number} index - 行索引
+     * 删除物料行(分页适配)
+     * @param {import('./types').ForecastFormMixinData['stockTableData'][number]} row
+     * @param {number} index - 当前页内索引
      * @returns {Promise<void>}
-     * @this {import('./types').ForecastFormMixinComponent & Vue}
      */
     async handleDelete(row, index) {
       try {
-        // 索引校验,必要时根据唯一标识兜底定位
-        let removeIndex = typeof index === 'number' ? index : -1
-        if (removeIndex < 0 || removeIndex >= this.stockTableData.length) {
-          const keyId = row && (row.id != null ? row.id : row.goodsId)
+        // 先通过唯一键定位(优先 id,其次 goodsId)
+        const keyId = row && (row.id != null ? row.id : row.goodsId)
+        let removeIndex = -1
+        if (keyId != null) {
           removeIndex = this.stockTableData.findIndex(r => (r.id != null ? r.id : r.goodsId) === keyId)
         }
+        // 如果无唯一键或未找到,则按分页换算全局索引
+        if (removeIndex < 0 && typeof index === 'number') {
+          const globalIndex = (Math.max(1, Number(this.currentPage)) - 1) * Math.max(1, Number(this.pageSize)) + index
+          if (globalIndex >= 0 && globalIndex < this.stockTableData.length) {
+            removeIndex = globalIndex
+          }
+        }
         if (removeIndex < 0) {
           this.$message && this.$message.warning('未定位到要删除的记录')
           return
         }
 
-        // 二次确认
         await this.$confirm('确认删除该物料吗?删除后可重新通过上方选择器导入。', '提示', {
           type: 'warning',
           confirmButtonText: '删除',
           cancelButtonText: '取消'
         })
 
-        // 使用 Vue.set/delete 保持响应式
+        // 记录待删除行的唯一键,用于同步选择状态
+        const removedKey = this.getRowUniqueKey(row)
+
         this.$delete(this.stockTableData, removeIndex)
 
-        // 如有需要,清理与该行相关的临时状态(当前实现无行级临时状态)
-        // 例如:this.currentInventory = null
+        // 同步移除选择状态中的该行
+        if (removedKey != null) {
+          this.selectedRowKeys = (Array.isArray(this.selectedRowKeys) ? this.selectedRowKeys : []).filter(k => k !== removedKey)
+        }
+
+        // 删除后校正页码:若当前页无数据则回退到上一页
+        this.normalizePageAfterMutations()
 
-        this.$message && this.$message.success('已删除')
+        // 删除后更新下拉选项(被删除的物料重新回到可选择项)
+        this.updateStockSelectOptions()
+
+        // [A] removed toast: deletion success message suppressed by requirement
       } catch (e) {
         // 用户取消不提示为错误,其他情况做日志记录
         if (e && e !== 'cancel') {
@@ -1284,6 +1366,183 @@ export default {
     },
 
     /**
+     * 表格选择变更(el-table @selection-change)
+     * @param {Array<import('./types').ForecastFormMixinData['stockTableData'][number]>} selection
+     * @returns {void}
+     */
+    handleSelectionChange(selection) {
+      // 程序化同步时不触发合并逻辑,避免回调环
+      if (this.selectionSyncing) return
+      // 基于当前页数据与新选择集,维护跨页 selection 的“并集 - 当前页未选差集”
+      const currentPageRows = Array.isArray(this.pagedStockTableData) ? this.pagedStockTableData : []
+      const currentPageKeys = new Set(
+        currentPageRows
+          .map(r => this.getRowUniqueKey(r))
+          .filter(k => k !== undefined && k !== null && k !== '')
+      )
+
+      const nextSelectedOnPage = new Set(
+        Array.isArray(selection)
+          ? selection
+              .map(r => this.getRowUniqueKey(r))
+              .filter(k => k !== undefined && k !== null && k !== '')
+          : []
+      )
+
+      const prev = Array.isArray(this.selectedRowKeys) ? this.selectedRowKeys : []
+      const union = new Set(prev)
+
+      // 1) 移除当前页中被取消勾选的键
+      currentPageKeys.forEach(k => {
+        if (!nextSelectedOnPage.has(k)) {
+          union.delete(k)
+        }
+      })
+      // 2) 加入当前页新勾选的键
+      nextSelectedOnPage.forEach(k => {
+        union.add(k)
+      })
+
+      this.selectedRowKeys = Array.from(union)
+    },
+
+    /**
+     * 行唯一键生成函数(绑定给 :row-key)
+     * 更健壮的容错:依次尝试 id -> goodsId -> itemId -> code -> itemCode -> 组合键(cname/itemName + code/itemCode + brandCode + typeNo/specs)
+     * @param {import('./types').ForecastFormMixinData['stockTableData'][number]} row
+     * @returns {string | number}
+     */
+    getRowUniqueKey(row) {
+      if (!row || typeof row !== 'object') return ''
+      /** @type {any} */
+      const anyRow = /** @type {any} */ (row)
+      // 简化:仅按 id -> goodsId -> code 顺序
+      if (anyRow.id !== undefined && anyRow.id !== null) return /** @type {any} */ (anyRow.id)
+      if (anyRow.goodsId !== undefined && anyRow.goodsId !== null) return /** @type {any} */ (anyRow.goodsId)
+      if (anyRow.code) return String(anyRow.code)
+      return ''
+    },
+
+    /**
+     * 更新物料下拉选项(过滤已在表格中的物料)
+     * @returns {void}
+     */
+    updateStockSelectOptions() {
+      try {
+        const table = Array.isArray(this.stockTableData) ? this.stockTableData : []
+        const source = Array.isArray(this.stockDescList) ? this.stockDescList : []
+
+        const idSet = new Set(table.filter(r => r && r.id !== undefined && r.id !== null).map(r => String(r.id)))
+        const goodsIdSet = new Set(table.filter(r => r && r.goodsId !== undefined && r.goodsId !== null).map(r => String(r.goodsId)))
+        const codeSet = new Set(table.filter(r => r && r.code).map(r => String(r.code)))
+
+        const options = source
+          .filter(item => {
+            const byId = item && item.id !== undefined && item.id !== null && idSet.has(String(item.id))
+            const byGoods = item && item.goodsId !== undefined && item.goodsId !== null && goodsIdSet.has(String(item.goodsId))
+            const byCode = item && item.code && codeSet.has(String(item.code))
+            return !(byId || byGoods || byCode)
+          })
+          .map(item => ({
+            label: /** @type {any} */ (item.cname || item.code || ''),
+            value: /** @type {any} */ (String(item.id))
+          }))
+
+        this.stockSelectOptions = options
+        // 如果当前选中不在可选项中,则清空
+        const hasSelected = options.some(opt => opt && opt.value === this.selectedStockId)
+        if (!hasSelected) {
+          this.selectedStockId = null
+        }
+      } catch (e) {
+        console.warn('更新物料下拉选项失败:', e)
+      }
+    },
+
+    /**
+     * 批量删除已选中的物料
+     * @returns {Promise<void>}
+     */
+    async handleBatchDelete() {
+      try {
+        const keys = new Set(Array.isArray(this.selectedRowKeys) ? this.selectedRowKeys : [])
+        if (keys.size === 0) {
+          this.$message && this.$message.warning('请先在下方表格选择要删除的物料')
+          return
+        }
+
+        await this.$confirm('确认删除已选中的物料吗?删除后可重新通过上方选择器导入。', '提示', {
+          type: 'warning',
+          confirmButtonText: '删除',
+          cancelButtonText: '取消'
+        })
+
+        const filtered = (Array.isArray(this.stockTableData) ? this.stockTableData : []).filter(row => {
+          const key = this.getRowUniqueKey(row)
+          return !keys.has(key)
+        })
+        this.stockTableData = filtered
+
+        // 清空选择并校正页码
+        this.selectedRowKeys = []
+        this.normalizePageAfterMutations()
+        // 刷新下拉选项
+        this.updateStockSelectOptions()
+
+        // [A] removed toast: batch deletion success message suppressed by requirement
+      } catch (e) {
+        if (e && e !== 'cancel') {
+          console.error('批量删除失败:', e)
+          this.$message && this.$message.error('批量删除失败,请稍后重试')
+        }
+      }
+    },
+
+    /**
+     * 页容量变更(el-pagination: size-change)
+     * @param {number} size
+     * @returns {void}
+     */
+    handleSizeChange(size) {
+      const newSize = Number(size) > 0 ? Number(size) : 10
+      this.pageSize = newSize
+      // 变更每页大小后,将页码重置为 1
+      this.currentPage = 1
+      // 同步当前页表格的选择回显
+      this.syncTableSelection()
+    },
+
+    /**
+     * 页码变更(el-pagination: current-change)
+     * @param {number} page
+     * @returns {void}
+     */
+    handlePageChange(page) {
+      const newPage = Number(page) > 0 ? Number(page) : 1
+      this.currentPage = newPage
+      // 同步当前页表格的选择回显
+      this.syncTableSelection()
+    },
+
+    /**
+     * 变更后校正页码(删除/导入后调用)
+     * @returns {void}
+     */
+    normalizePageAfterMutations() {
+      const total = this.total
+      const size = Math.max(1, Number(this.pageSize) || 10)
+      const maxPage = Math.max(1, Math.ceil(total / size))
+      if (this.currentPage > maxPage) {
+        this.currentPage = maxPage
+      }
+      if (this.currentPage < 1) {
+        this.currentPage = 1
+      }
+      // 校正页码后,确保当前页的表格勾选状态与 selectedRowKeys 保持一致
+      this.syncTableSelection()
+    },
+
+    /**
      * 品牌变更处理
      * @param {number} brandId - 品牌ID
      * @returns {void}
@@ -1300,6 +1559,12 @@ export default {
         this.formData.brandCode = ''
         this.formData.brandName = ''
       }
+      // 品牌切换后清空选中行,刷新下拉选项并同步表格选择
+      this.selectedRowKeys = []
+      this.$nextTick(() => {
+        this.syncTableSelection && this.syncTableSelection()
+      })
+      this.updateStockSelectOptions && this.updateStockSelectOptions()
     },
 
     /**
@@ -1396,6 +1661,40 @@ export default {
         // 异常时不阻塞新增,默认允许保存
         this.$emit && this.$emit(FORECAST_FORM_EVENTS.SAVE_DISABLED_CHANGE, false)
       }
-    }
+    },
+
+    /**
+     * 程序化同步当前页表格勾选状态到 selectedRowKeys
+     * @returns {void}
+     */
+    syncTableSelection() {
+      try {
+        /** @type {any} */
+        const table = this.$refs && this.$refs.stockTable
+        if (!table || typeof table.clearSelection !== 'function' || typeof table.toggleRowSelection !== 'function') return
+        this.selectionSyncing = true
+        this.$nextTick(() => {
+          try {
+            table.clearSelection()
+            const selectedSet = new Set(Array.isArray(this.selectedRowKeys) ? this.selectedRowKeys : [])
+            const rows = Array.isArray(this.pagedStockTableData) ? this.pagedStockTableData : []
+            rows.forEach(row => {
+              const key = this.getRowUniqueKey(row)
+              if (selectedSet.has(key)) {
+                table.toggleRowSelection(row, true)
+              }
+            })
+          } catch (e) {
+            console.warn('同步表格选择异常:', e)
+          } finally {
+            this.selectionSyncing = false
+          }
+        })
+      } catch (e) {
+        console.warn('syncTableSelection 初始化失败:', e)
+        this.selectionSyncing = false
+      }
+    },
   }
 }
+

+ 33 - 5
src/components/forecast-form/index.vue

@@ -79,15 +79,28 @@
               style="margin-left: 8px"
               @click="handleImportSelectedStock"
             >添加物料</el-button>
+            <!-- 批量删除按钮:始终展示,无选中或加载中禁用 -->
+            <el-button
+              type="danger"
+              icon="el-icon-delete"
+              :disabled="tableLoading || !hasSelection"
+              style="margin-left: 8px"
+              @click="handleBatchDelete"
+            >批量删除</el-button>
           </div>
 
           <el-table
-            :data="stockTableData"
+            ref="stockTable"
+            :data="pagedStockTableData"
             border
             stripe
             height="360"
             v-loading="tableLoading"
+            :row-key="getRowUniqueKey"
+            :reserve-selection="true"
+            @selection-change="handleSelectionChange"
           >
+            <el-table-column type="selection" width="48" />
             <el-table-column prop="code" label="物料编码" min-width="140" show-overflow-tooltip />
             <el-table-column prop="cname" label="物料名称" min-width="160" show-overflow-tooltip />
             <el-table-column prop="brandName" label="品牌名称" min-width="120" show-overflow-tooltip />
@@ -122,6 +135,20 @@
               </template>
             </el-table-column>
           </el-table>
+
+          <!-- 分页控件(前端分页) -->
+          <div class="table-pagination" style="margin-top: 8px; text-align: right;">
+            <el-pagination
+              :current-page="currentPage"
+              :page-size="pageSize"
+              :page-sizes="[5, 10, 20, 50, 100]"
+              :total="total"
+              layout="total, sizes, prev, pager, next, jumper"
+              @current-change="handlePageChange"
+              @size-change="handleSizeChange"
+            />
+          </div>
+
           <div class="table-tip">提示:先在上方选择物料并点击“导入物料”,导入后的数据将显示在表格并参与保存流程。</div>
         </div>
       </div>
@@ -174,8 +201,8 @@ export default {
     this.$nextTick(() => {
       try {
         if (this.isEdit) {
-          if (this.$refs.yearPicker) this.$refs.yearPicker.disabled = true
-          if (this.$refs.monthSelect) this.$refs.monthSelect.disabled = true
+          if (this.$refs.yearPicker) { /** @type {any} */ (this.$refs.yearPicker).disabled = true }
+          if (this.$refs.monthSelect) { /** @type {any} */ (this.$refs.monthSelect).disabled = true }
         }
       } catch (e) {
         // 忽略可能的非关键性异常
@@ -185,11 +212,12 @@ export default {
 
   methods: {
     // 通过 $refs 统一设置“年份/月份”的禁用状态
+    /** @param {boolean} disabled */
     setYearMonthDisabled(disabled) {
       this.$nextTick(() => {
         try {
-          if (this.$refs.yearPicker) this.$refs.yearPicker.disabled = disabled
-          if (this.$refs.monthSelect) this.$refs.monthSelect.disabled = disabled
+          if (this.$refs.yearPicker) { /** @type {any} */ (this.$refs.yearPicker).disabled = disabled }
+          if (this.$refs.monthSelect) { /** @type {any} */ (this.$refs.monthSelect).disabled = disabled }
         } catch (e) {
           // 忽略可能的非关键性异常
         }

+ 41 - 1
src/components/forecast-form/types.d.ts

@@ -1,3 +1,10 @@
+/// <reference path="../../types/global.d.ts" />
+
+import type { AxiosResponse } from 'axios'
+import type { ApiResponse, PageResult, SelectOption, ValidationRule } from '@/api/types/forecast'
+import type { ItemRecord } from '@/api/types/common'
+import type { AvueFormColumn, AvueFormOption } from '@types/smallwei__avue/form'
+
 /**
  * 销售预测表单组件类型定义
  * @description 为销售预测表单相关组件提供完整的TypeScript类型支持
@@ -169,6 +176,15 @@ export interface ForecastFormMixinData {
   stockSelectOptions: Array<SelectOption<string>>;
   /** 当前选择待导入的物料ID */
   selectedStockId: string | null;
+  /** 选择的行唯一键集合(跨分页保持) */
+  selectedRowKeys: Array<string | number>;
+  /** 内部标记:是否正在进行程序化的表格选择同步,避免误触发 selection-change 逻辑 */
+  selectionSyncing: boolean;
+
+  /** 分页:当前页(从1开始) */
+  currentPage: number;
+  /** 分页:每页条数 */
+  pageSize: number;
 }
 
 /**
@@ -234,6 +250,12 @@ export interface ForecastFormProps {
 export interface ForecastFormComputed {
   /** 表单标题 */
   formTitle: string;
+  /** 物料总数(用于分页total) */
+  total: number;
+  /** 当前页展示的数据(分页后) */
+  pagedStockTableData: Array<Partial<import('@/api/types/order').PjpfStockDesc> & { forecastQuantity: number; brandCode?: string; storeInventory?: string }>;
+  /** 是否有选中项(用于禁用批量删除按钮) */
+  hasSelection: boolean;
 }
 
 /**
@@ -280,8 +302,17 @@ export interface ForecastFormMethods {
   handleImportSelectedStock(): void;
   /** 删除物料行 */
   handleDelete(row: ForecastFormMixinData['stockTableData'][number], index: number): Promise<void>;
+  // 分页事件处理
+  /** 页面容量变更 */
+  handleSizeChange(size: number): void;
+  /** 页码变更 */
+  handlePageChange(page: number): void;
+  /** 变更后校正页码(删除/导入后) */
+  normalizePageAfterMutations(): void;
   /** 合并编辑回显行的库存数量 */
   mergeEchoStoreInventory(): Promise<void>;
+  /** 新增模式:检查指定年月是否已有预测,并通过事件通知父组件控制保存按钮禁用 */
+  checkForecastByMonthAndEmit(): Promise<void>;
   /** 提交(Avue 回调) */
   handleSubmit(): void;
   /** 重置 */
@@ -292,6 +323,15 @@ export interface ForecastFormMethods {
   loadCurrentCustomerInfo(): Promise<void>;
   /** 可选:宿主组件用于禁用年月选择器的辅助方法 */
   setYearMonthDisabled?: (disabled: boolean) => void;
+  /** 表格选择变更(跨分页维护 selectedRowKeys) */
+  handleSelectionChange(selection: Array<ForecastFormMixinData['stockTableData'][number]>): void;
+  /** 程序化同步表格选择(根据 selectedRowKeys 回显当前页选择) */
+  syncTableSelection?: () => void;
+  getRowUniqueKey(row: ForecastFormMixinData['stockTableData'][number]): string | number;
+  /** 更新物料下拉选项:过滤已在表格中的物料 */
+  updateStockSelectOptions(): void;
+  /** 批量删除选中物料 */
+  handleBatchDelete(): Promise<void>;
 }
 
 /**
@@ -328,4 +368,4 @@ export interface CustomerSelectData {
 /**
  * 混入组件最终类型(给 JSDoc 使用)
  */
-export interface ForecastFormMixinComponent extends ForecastFormProps, ForecastFormMixinData, ForecastFormComputed, ForecastFormMethods, Vue {}
+export interface ForecastFormMixinComponent extends ForecastFormProps, ForecastFormMixinData, ForecastFormComputed, ForecastFormMethods, import('vue/types/vue').Vue {}

+ 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;

+ 16 - 2
src/views/claim/index.vue

@@ -344,8 +344,8 @@
       <el-form :model="auditForm" :rules="auditFormRules" ref="auditFormRef" label-width="120px">
         <el-form-item label="审核结果" prop="auditResult">
           <el-select v-model="auditForm.auditResult" placeholder="请选择审核结果" style="width: 100%;">
-            <el-option :label="AUDIT_STATUS_OPTIONS.find(o => o.value === AUDIT_STATUS.APPROVED)?.label || '通过'" :value="AUDIT_STATUS.APPROVED" />
-            <el-option :label="AUDIT_STATUS_OPTIONS.find(o => o.value === AUDIT_STATUS.REJECTED)?.label || '拒绝'" :value="AUDIT_STATUS.REJECTED" />
+            <el-option :label="getAuditStatusLabel(AUDIT_STATUS.APPROVED, '通过')" :value="AUDIT_STATUS.APPROVED" />
+            <el-option :label="getAuditStatusLabel(AUDIT_STATUS.REJECTED, '拒绝')" :value="AUDIT_STATUS.REJECTED" />
           </el-select>
         </el-form-item>
         <el-form-item label="审核金额" prop="auditAmount">
@@ -407,6 +407,20 @@ export default {
       AUDIT_STATUS,
       AUDIT_STATUS_OPTIONS
     };
+  },
+  methods: {
+    getAuditStatusLabel: function(value, fallback) {
+      var opts = this.AUDIT_STATUS_OPTIONS || [];
+      var fb = (typeof fallback === 'string') ? fallback : '';
+      for (var i = 0; i < opts.length; i++) {
+        var item = opts[i];
+        if (item && item.value === value) {
+          var label = item.label;
+          return (typeof label === 'string' && label) ? label : fb;
+        }
+      }
+      return fb;
+    }
   }
 }
 </script>

+ 2 - 2
vue.config.js

@@ -56,9 +56,9 @@ module.exports = {
       '/api': {
         //本地服务接口地址
         // target: 'http://192.168.8.101:1080',
-        target: 'http://192.168.8.114:1080',
+        // target: 'http://192.168.8.114:1080',
         // target: 'http://127.0.0.1:1080',
-        // target: 'http://gubersial-api.cpolar.top',
+        target: 'http://gubersial-api.cpolar.top',
         // target: 'http://gubersial-ui.cpolar.top',
         // target: 'https://22687sh999.goho.co',
         ws: true,