feat(stocktake): implement strict blind stocktake logic with hidden system qty, editable count and status filters

This commit is contained in:
DXC
2026-03-26 17:42:51 +08:00
parent db0444cc13
commit 2a27f2e0df
3 changed files with 348 additions and 28 deletions

View File

@ -210,21 +210,30 @@
<el-drawer
v-model="showList"
title="📦 盘点清单 (点击修改)"
title="📦 盘点清单"
direction="btt"
size="100%"
destroy-on-close
class="inventory-drawer"
>
<div class="drawer-layout" v-loading="listLoading">
<div class="search-bar">
<el-input v-model="listKeyword" placeholder="搜索 SKU..." :prefix-icon="Search" clearable @keyup.enter="handleListSearch" @clear="handleListSearch" style="width: 240px" />
<!-- 搜索和状态筛选 -->
<div class="search-bar" style="display: flex; align-items: center; gap: 10px; margin-bottom: 15px;">
<el-input v-model="listKeyword" placeholder="搜索 SKU/名称..." :prefix-icon="Search" clearable @keyup.enter="handleListSearch" @clear="handleListSearch" style="width: 240px" />
<el-button type="primary" @click="handleListSearch">搜索</el-button>
<el-radio-group v-model="listStatusFilter" @change="handleListSearch" size="small">
<el-radio-button value="all">全部</el-radio-button>
<el-radio-button value="counted">已盘</el-radio-button>
<el-radio-button value="uncounted">未盘</el-radio-button>
</el-radio-group>
<span style="margin-left: auto; color: #909399; font-size: 13px;">
总品项: {{ stats.total }} | 已盘: {{ stats.scanned }} | 未盘: {{ stats.total - stats.scanned }}
</span>
</div>
<div class="table-container">
<el-table
:data="listData"
:data="filteredListData"
height="100%"
stripe
border
@ -234,16 +243,33 @@
<el-table-column prop="sku" label="SKU" width="140" show-overflow-tooltip />
<el-table-column prop="material_name" label="名称" min-width="120" show-overflow-tooltip />
<el-table-column prop="spec_model" label="规格" width="120" show-overflow-tooltip />
<el-table-column prop="stock_qty" label="账面数" width="80" align="center" />
<el-table-column prop="quantity" label="实盘数" width="80" align="center" />
<el-table-column label="差异" width="80" align="center">
<!-- 盲盘隐藏账面数和差异列 -->
<!-- <el-table-column prop="stock_qty" label="账面数" width="80" align="center" /> -->
<el-table-column label="实盘数" width="100" align="center">
<template #default="{ row }">
<el-input-number
v-model="row.quantity"
:min="0"
:step="1"
size="small"
controls-position="right"
@change="(val) => handleQuantityChange(row, val)"
:disabled="!userStore.hasPermission('inventory_stocktake:operation')"
/>
</template>
</el-table-column>
<!-- <el-table-column label="差异" width="80" align="center">
<template #default="{ row }">
<span :style="{ color: row.diff_qty > 0 ? '#67C23A' : row.diff_qty < 0 ? '#F56C6C' : '#909399' }">
{{ row.diff_qty > 0 ? '+' : '' }}{{ row.diff_qty || 0 }}
</span>
</template>
</el-table-column> -->
<el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip>
<template #default="{ row }">
<el-input v-model="row.remark" placeholder="备注" size="small" @blur="handleRemarkChange(row)" />
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
</el-table>
</div>
@ -253,7 +279,7 @@
v-model:current-page="listPage"
v-model:page-size="listLimit"
:page-sizes="[20, 50, 100, 200]"
:total="listTotal"
:total="listTotalFiltered"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleListLimitChange"
@current-change="handleListPageChange"
@ -386,7 +412,7 @@
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
import { getStockList } from '@/api/inbound/stock'
import { getStockList, getAllStocktakeItems, updateStocktakeQuantity } from '@/api/inbound/stock'
import QrScanner from '@/components/QrScanner/index.vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, VideoPlay, VideoPause, List, Checked, Download, ArrowRight, Cloudy, Edit, EditPen, CameraFilled, Close, WarningFilled } from '@element-plus/icons-vue'
@ -448,22 +474,75 @@ const listTotal = ref(0)
const listKeyword = ref('')
const listLoading = ref(false)
const listData = ref<any[]>([])
// ★ 新增: 盘点开始防呆倒计时
const countdown = ref(0)
let countdownTimer: any = null
// ★ 新增: 防呆确认弹窗显示状态
const showConfirmDialog = ref(false)
// ★★★ 核心修改:只存储已扫码的物料列表,不再缓存全量库存 ★★★
const tableData = ref<StockItem[]>([])
const scannedMap = ref<Map<string, number>>(new Map())
const borrowedQuantities = ref<Record<string, number>>({})
const listStatusFilter = ref<'all' | 'counted' | 'uncounted'>('all')
const allStockItems = ref<any[]>([]) // 全量应盘物资(盘点基数)
const listTotalFiltered = ref(0) // 过滤后的总数
// ★ 新增: 会话ID
const currentSessionId = ref<string>('')
// 获取应盘物资清单(盘点基数)
const fetchAllStockItems = async () => {
try {
const res: any = await getAllStocktakeItems()
if (res && res.code === 200) {
allStockItems.value = res.data.items || []
}
} catch (e) {
console.error('获取应盘物资清单失败', e)
}
}
// 过滤已在 fetchInventoryList 中处理,此处不再需要 computed
// const filteredListData = computed(() => {
// let items = [...listData.value]
//
// if (listStatusFilter.value === 'counted') {
// items = items.filter(item => item.quantity > 0)
// } else if (listStatusFilter.value === 'uncounted') {
// items = items.filter(item => !item.quantity || item.quantity === 0)
// }
//
// return items
// })
// 统计信息:从全量应盘物资中计算
const stats = computed(() => {
// 已盘点数量:从已扫描的记录中获取
const countedItems = new Set()
listData.value.forEach(item => {
if (item.stock_id && item.source_table) {
countedItems.add(`${item.source_table}-${item.stock_id}`)
}
})
// 全量应盘物资中已盘的数量
let scanned = 0
allStockItems.value.forEach(item => {
const key = `${item.source_table}-${item.id}`
if (countedItems.has(key)) {
scanned++
}
})
// 如果 allStockItems 为空,回退到旧的统计逻辑
if (allStockItems.value.length === 0) {
const total = listData.value.length
const scannedCount = listData.value.filter(i => i.quantity > 0).length
return {
total,
scanned: scannedCount,
varianceItems: 0
}
}
return {
total: allStockItems.value.length,
scanned: scanned,
varianceItems: 0
}
})
const varianceLoading = ref(false)
const currentItem = ref<StockItem | null>(null)
@ -960,23 +1039,67 @@ const filteredVarianceList = computed(() => {
)
})
// ★ 新增: 获取盘点清单数据(后端分页
// ★ 新增: 获取盘点清单数据(合并全量应盘物资 + 已盘点记录
const fetchInventoryList = async () => {
listLoading.value = true
try {
// 1. 获取已盘点记录
const res: any = await request({
url: '/v1/inbound/stock/draft/list',
method: 'get',
params: {
page: listPage.value,
limit: listLimit.value,
page: 1,
limit: 10000, // 获取全部已盘点记录
keyword: listKeyword.value
}
})
if (res) {
listData.value = res.items || []
listTotal.value = res.total || 0
const scannedDrafts = res?.items || []
// 2. 使用全量应盘物资列表
// 对于每个应盘物资,检查是否有对应的盘点记录
let mergedData = allStockItems.value.map(item => {
// 查找对应的盘点记录
const draft = scannedDrafts.find((d: any) =>
d.source_table === item.source_table && d.stock_id === item.id
)
return {
id: draft?.id || null,
stock_id: item.id,
source_table: item.source_table,
sku: item.sku,
material_name: item.material_name,
spec_model: item.spec_model,
stock_qty: item.stock_qty, // 账面数(盲盘时隐藏)
quantity: draft?.quantity || 0, // 实盘数
diff_qty: draft ? (draft.quantity - item.stock_qty) : -item.stock_qty, // 差异
remark: draft?.remark || '',
warehouse_location: item.warehouse_location
}
})
// 3. 关键词过滤
if (listKeyword.value) {
const kw = listKeyword.value.toLowerCase()
mergedData = mergedData.filter((item: any) =>
item.sku?.toLowerCase().includes(kw) ||
item.material_name?.toLowerCase().includes(kw)
)
}
// 4. 状态过滤
if (listStatusFilter.value === 'counted') {
mergedData = mergedData.filter((item: any) => item.quantity > 0)
} else if (listStatusFilter.value === 'uncounted') {
mergedData = mergedData.filter((item: any) => !item.quantity || item.quantity === 0)
}
// 5. 分页
listTotalFiltered.value = mergedData.length
const start = (listPage.value - 1) * listLimit.value
listData.value = mergedData.slice(start, start + listLimit.value)
} catch (e) {
ElMessage.error('获取盘点清单失败')
} finally {
@ -989,6 +1112,10 @@ const openInventoryList = async () => {
showList.value = true
listPage.value = 1
listKeyword.value = ''
listStatusFilter.value = 'all'
// 获取盘点基数(应盘物资清单)
await fetchAllStockItems()
// 获取已盘点列表
await fetchInventoryList()
}
@ -1010,6 +1137,30 @@ const handleListLimitChange = (limit: number) => {
fetchInventoryList()
}
// ★ 新增: 更新实盘数量(手动修改)
const handleQuantityChange = async (row: any, val: number) => {
try {
await updateStocktakeQuantity({
stock_id: row.stock_id,
source_table: row.source_table,
quantity: val
})
ElMessage.success('实盘数已更新')
} catch (e) {
console.error('更新实盘数失败', e)
ElMessage.error('更新失败')
// 重新获取列表数据
fetchInventoryList()
}
}
// ★ 新增: 更新备注
const handleRemarkChange = async (row: any) => {
// 备注更新可以批量处理或直接调用后端接口
// 这里暂时只更新本地数据,实际项目中可以调用后端保存
console.log('备注更新:', row.remark, row.id)
}
// ★ 修改:结束盘点按钮直接调用 finishStocktake跳过二次确认弹窗
const openFinishDialog = () => {
if (stats.value.total === 0) {