浏览代码

feat(forecast-form): 添加批量删除物料功能及跨页选择支持

yz 4 天之前
父节点
当前提交
988bd72cb9

+ 164 - 9
src/components/forecast-form/forecast-form-mixin.js

@@ -284,6 +284,10 @@ export default {
        */
       selectedStockId: null,
 
+      // 选择状态:存储已选中的行唯一键(跨分页)
+      /** @type {Array<string|number>} */
+      selectedRowKeys: [],
+
       /** 当前库存 */
       currentInventory: null,
 
@@ -332,6 +336,12 @@ export default {
       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
     }
   },
 
@@ -1200,6 +1210,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) || []
@@ -1207,13 +1219,10 @@ 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()
       } catch (e) {
         console.error('加载用户关联商品失败:', e)
         this.$message.error(e.message || '加载用户关联商品失败')
@@ -1235,7 +1244,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
@@ -1244,15 +1253,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
@@ -1268,6 +1277,8 @@ export default {
       this.stockTableData.push({ ...stock, forecastQuantity: 0 })
       // 清空已选
       this.selectedStockId = null
+      // 导入后更新下拉选项(过滤掉已在表格中的物料)
+      this.updateStockSelectOptions()
 
       // 导入后保持当前页不变;无需校正页码(不会超过最大页)
     },
@@ -1304,11 +1315,22 @@ export default {
           cancelButtonText: '取消'
         })
 
+        // 记录待删除行的唯一键,用于同步选择状态
+        const removedKey = this.getRowUniqueKey(row)
+
         this.$delete(this.stockTableData, removeIndex)
 
+        // 同步移除选择状态中的该行
+        if (removedKey != null) {
+          this.selectedRowKeys = (Array.isArray(this.selectedRowKeys) ? this.selectedRowKeys : []).filter(k => k !== removedKey)
+        }
+
         // 删除后校正页码:若当前页无数据则回退到上一页
         this.normalizePageAfterMutations()
 
+        // 删除后更新下拉选项(被删除的物料重新回到可选择项)
+        this.updateStockSelectOptions()
+
         this.$message && this.$message.success('已删除')
       } catch (e) {
         // 用户取消不提示为错误,其他情况做日志记录
@@ -1320,6 +1342,139 @@ export default {
     },
 
     /**
+     * 表格选择变更(el-table @selection-change)
+     * @param {Array<import('./types').ForecastFormMixinData['stockTableData'][number]>} selection
+     * @returns {void}
+     */
+    handleSelectionChange(selection) {
+      // 基于当前页数据与新选择集,维护跨页 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)
+     * @param {import('./types').ForecastFormMixinData['stockTableData'][number]} row
+     * @returns {string | number}
+     */
+    getRowUniqueKey(row) {
+      if (!row || typeof row !== 'object') return ''
+      // 优先使用后端提供的 id
+      if (row.id !== undefined && row.id !== null) return /** @type {any} */ (row.id)
+      // 其次使用 goodsId
+      if (row.goodsId !== undefined && row.goodsId !== null) return /** @type {any} */ (row.goodsId)
+      // 再次使用 code
+      if (row.code) return String(row.code)
+      // 兜底:用可读信息组合
+      const name = /** @type {any} */ (row.cname || '')
+      const code = /** @type {any} */ (row.code || '')
+      return `${String(name)}-${String(code)}`
+    },
+
+    /**
+     * 更新物料下拉选项(过滤已在表格中的物料)
+     * @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()
+
+        this.$message && this.$message.success('已删除所选物料')
+      } catch (e) {
+        if (e && e !== 'cancel') {
+          console.error('批量删除失败:', e)
+          this.$message && this.$message.error('批量删除失败,请稍后重试')
+        }
+      }
+    },
+
+    /**
      * 页容量变更(el-pagination: size-change)
      * @param {number} size
      * @returns {void}

+ 13 - 0
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
+            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="55" align="center" />
             <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 />

+ 12 - 0
src/components/forecast-form/types.d.ts

@@ -176,6 +176,8 @@ export interface ForecastFormMixinData {
   stockSelectOptions: Array<SelectOption<string>>;
   /** 当前选择待导入的物料ID */
   selectedStockId: string | null;
+  /** 选择的行唯一键集合(跨分页保持) */
+  selectedRowKeys: Array<string | number>;
 
   /** 分页:当前页(从1开始) */
   currentPage: number;
@@ -250,6 +252,8 @@ export interface ForecastFormComputed {
   total: number;
   /** 当前页展示的数据(分页后) */
   pagedStockTableData: Array<Partial<import('@/api/types/order').PjpfStockDesc> & { forecastQuantity: number; brandCode?: string; storeInventory?: string }>;
+  /** 是否有选中项(用于禁用批量删除按钮) */
+  hasSelection: boolean;
 }
 
 /**
@@ -317,6 +321,14 @@ export interface ForecastFormMethods {
   loadCurrentCustomerInfo(): Promise<void>;
   /** 可选:宿主组件用于禁用年月选择器的辅助方法 */
   setYearMonthDisabled?: (disabled: boolean) => void;
+  /** 表格选择变更(跨分页维护 selectedRowKeys) */
+  handleSelectionChange(selection: Array<ForecastFormMixinData['stockTableData'][number]>): void;
+  /** 行唯一键生成函数(绑定给 :row-key) */
+  getRowUniqueKey(row: ForecastFormMixinData['stockTableData'][number]): string | number;
+  /** 更新物料下拉选项:过滤已在表格中的物料 */
+  updateStockSelectOptions(): void;
+  /** 批量删除选中物料 */
+  handleBatchDelete(): Promise<void>;
 }
 
 /**