|
@@ -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
|
|
|
+ }
|
|
|
+ },
|
|
|
}
|
|
|
}
|
|
|
+
|