refactor(stocktake): completely remove local stock cache and implement real-time remote scan match
This commit is contained in:
@ -456,88 +456,21 @@ let countdownTimer: any = null
|
||||
// ★ 新增: 防呆确认弹窗显示状态
|
||||
const showConfirmDialog = ref(false)
|
||||
|
||||
const allData = ref<StockItem[]>([])
|
||||
// ★★★ 核心修改:只存储已扫码的物料列表,不再缓存全量库存 ★★★
|
||||
const tableData = ref<StockItem[]>([])
|
||||
const scannedMap = ref<Map<string, number>>(new Map())
|
||||
const borrowedQuantities = ref<Record<string, number>>({})
|
||||
|
||||
// ★ 新增: 分页查询参数
|
||||
const queryParams = reactive({
|
||||
page: 1,
|
||||
limit: 20,
|
||||
keyword: ''
|
||||
})
|
||||
const total = ref(0)
|
||||
|
||||
// ★ 新增: 会话ID
|
||||
const currentSessionId = ref<string>('')
|
||||
|
||||
const varianceLoading = ref(false)
|
||||
|
||||
const filterType = ref('all')
|
||||
const searchKeyword = ref('')
|
||||
|
||||
const currentItem = ref<StockItem | null>(null)
|
||||
const inputQty = ref<number | undefined>(undefined)
|
||||
const inputRemark = ref('')
|
||||
const qtyInputRef = ref()
|
||||
|
||||
// ★ 新增: 静默刷新数据(不弹loading)- 使用远程分页
|
||||
const syncData = async () => {
|
||||
try {
|
||||
// 使用较大 limit 获取足够数据用于扫码匹配(静默刷新)
|
||||
const res: any = await getStockList({
|
||||
page: 1,
|
||||
pageSize: 1000, // 获取足够多的数据用于扫码匹配
|
||||
keyword: '' // 静默刷新不传关键词
|
||||
})
|
||||
if (!res || !res.data) return
|
||||
|
||||
const processItem = (item: any, type: string) => {
|
||||
const stock = parseFloat(item.stock_quantity || item.qty_stock || 0)
|
||||
if (stock <= 0) return
|
||||
const name = item.name || item.material_name || item.product_name || '未知物品'
|
||||
const uuid = item.uuid || item.sku || ''
|
||||
const isScanned = scannedMap.value.has(uuid)
|
||||
return {
|
||||
...item,
|
||||
name: name,
|
||||
standard: item.spec_model || item.standard || item.model || '',
|
||||
sku: item.sku || '',
|
||||
uuid: uuid,
|
||||
bar_code: item.bar_code || item.barcode || '',
|
||||
qty_stock: stock,
|
||||
qty_actual: isScanned ? scannedMap.value.get(uuid)! : 0,
|
||||
scanned: isScanned,
|
||||
uniqueKey: `${type}_${item.id}`,
|
||||
source_table: typeToSourceTable(type),
|
||||
stock_id: item.id
|
||||
}
|
||||
}
|
||||
|
||||
const list: StockItem[] = []
|
||||
const items = res.data.list || []
|
||||
items.forEach((item: any) => {
|
||||
const type = item.stock_type || item.type || 'material'
|
||||
const processed = processItem(item, type)
|
||||
if (processed) list.push(processed)
|
||||
})
|
||||
|
||||
// ★ 强制按 SKU 数字+字符串升序排序
|
||||
list.sort((a, b) => {
|
||||
const skuA = a.sku || '';
|
||||
const skuB = b.sku || '';
|
||||
return skuA.localeCompare(skuB, undefined, { numeric: true, sensitivity: 'base' });
|
||||
});
|
||||
|
||||
// 静默更新数据(不触发loading)
|
||||
allData.value = list
|
||||
total.value = res.data.total || 0
|
||||
await fetchBorrowedQuantities(list)
|
||||
} catch (e) {
|
||||
// 静默失败,不弹错误
|
||||
}
|
||||
}
|
||||
|
||||
const api = {
|
||||
getDrafts: (sessionId?: string) => request({
|
||||
url: '/v1/inbound/stock/draft/list',
|
||||
@ -648,7 +581,7 @@ const doStartNewSession = async () => {
|
||||
const res: any = await api.startNewSession()
|
||||
currentSessionId.value = res.session_id || ''
|
||||
scannedMap.value.clear()
|
||||
await loadData()
|
||||
tableData.value = [] // 清空已扫码列表
|
||||
isSessionActive.value = true
|
||||
// ★ 标记当前阶段为 scanning(扫码中)
|
||||
localStorage.setItem('stocktake_phase', 'scanning')
|
||||
@ -705,9 +638,9 @@ const resumeSession = async () => {
|
||||
}
|
||||
})
|
||||
scannedMap.value = map
|
||||
|
||||
// 加载完整库存数据
|
||||
await loadData()
|
||||
|
||||
// 清空本地列表,用户扫码时会实时添加
|
||||
tableData.value = []
|
||||
|
||||
// ★ 智能路由:根据本地记忆的阶段决定下一步
|
||||
const phase = localStorage.getItem('stocktake_phase')
|
||||
@ -740,69 +673,8 @@ const returnToScan = () => {
|
||||
ElMessage.info('继续扫码,发现漏扫的物料')
|
||||
}
|
||||
|
||||
// ★★★ 核心修改:使用远程分页 API ★★★
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await getStockList({
|
||||
page: queryParams.page,
|
||||
pageSize: queryParams.limit,
|
||||
keyword: queryParams.keyword
|
||||
})
|
||||
|
||||
if (!res || !res.data) {
|
||||
allData.value = []
|
||||
total.value = 0
|
||||
return
|
||||
}
|
||||
|
||||
const list: StockItem[] = []
|
||||
const items = res.data.list || []
|
||||
|
||||
// 处理返回的库存数据
|
||||
items.forEach((item: any) => {
|
||||
const stock = parseFloat(item.stock_quantity || item.qty_stock || 0)
|
||||
if (stock <= 0) return
|
||||
|
||||
const type = item.stock_type || item.type || 'material'
|
||||
const uuid = item.uuid || item.sku || ''
|
||||
const isScanned = scannedMap.value.has(uuid)
|
||||
|
||||
list.push({
|
||||
...item,
|
||||
name: item.name || item.material_name || item.product_name || '未知物品',
|
||||
standard: item.spec_model || item.standard || item.model || '',
|
||||
sku: item.sku || '',
|
||||
batch_no: item.batch_no || item.batch_number || '',
|
||||
serial_number: item.serial_number || '',
|
||||
uuid: uuid,
|
||||
bar_code: item.bar_code || item.barcode || '',
|
||||
qty_stock: stock,
|
||||
qty_actual: isScanned ? scannedMap.value.get(uuid)! : 0,
|
||||
scanned: isScanned,
|
||||
uniqueKey: `${type}_${item.id}`,
|
||||
source_table: typeToSourceTable(type),
|
||||
stock_id: item.id
|
||||
})
|
||||
})
|
||||
|
||||
// ★ 强制按 SKU 数字+字符串升序排序
|
||||
list.sort((a, b) => {
|
||||
const skuA = a.sku || '';
|
||||
const skuB = b.sku || '';
|
||||
return skuA.localeCompare(skuB, undefined, { numeric: true, sensitivity: 'base' });
|
||||
});
|
||||
|
||||
allData.value = list
|
||||
total.value = res.data.total || 0
|
||||
|
||||
await fetchBorrowedQuantities(list)
|
||||
} catch (e) {
|
||||
ElMessage.error('数据加载失败')
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
const onScanSuccess = (code: string) => {
|
||||
// ★★★ 核心修改:扫码成功后实时查询后端匹配 ★★★
|
||||
const onScanSuccess = async (code: string) => {
|
||||
if (!code || loading.value) return
|
||||
const trimCode = code.trim()
|
||||
|
||||
@ -816,19 +688,61 @@ const onScanSuccess = (code: string) => {
|
||||
return
|
||||
}
|
||||
|
||||
const item = allData.value.find(i => i.uuid === trimCode || i.bar_code === trimCode)
|
||||
// 实时查询后端匹配
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await getStockList({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
keyword: trimCode
|
||||
})
|
||||
|
||||
if (!res || !res.data || !res.data.list || res.data.list.length === 0) {
|
||||
ElMessage.error(`未找到该物料库存: ${trimCode}`)
|
||||
if (navigator.vibrate) navigator.vibrate([200, 50, 200])
|
||||
return
|
||||
}
|
||||
|
||||
// 查找匹配的物料
|
||||
const foundItem = res.data.list.find((i: any) =>
|
||||
i.uuid === trimCode || i.sku === trimCode || i.barcode === trimCode || i.bar_code === trimCode
|
||||
)
|
||||
|
||||
if (!foundItem) {
|
||||
ElMessage.error(`未找到该物料库存: ${trimCode}`)
|
||||
if (navigator.vibrate) navigator.vibrate([200, 50, 200])
|
||||
return
|
||||
}
|
||||
|
||||
if (item) {
|
||||
if (navigator.vibrate) navigator.vibrate(100)
|
||||
|
||||
// ★★★ 核心修改:扫码成功后立即关闭全屏扫码,弹出填数对话框
|
||||
// 关闭全屏扫码,弹出填数对话框
|
||||
showCamera.value = false
|
||||
|
||||
// 无论是否多批次,都弹出对话框让用户确认数量
|
||||
// 处理数据格式
|
||||
const stock = parseFloat(foundItem.stock_quantity || foundItem.qty_stock || 0)
|
||||
const type = foundItem.stock_type || foundItem.type || 'material'
|
||||
const item: StockItem = {
|
||||
...foundItem,
|
||||
name: foundItem.name || foundItem.material_name || foundItem.product_name || '未知物品',
|
||||
standard: foundItem.spec_model || foundItem.standard || foundItem.model || '',
|
||||
sku: foundItem.sku || '',
|
||||
uuid: foundItem.uuid || foundItem.sku || '',
|
||||
bar_code: foundItem.bar_code || foundItem.barcode || '',
|
||||
qty_stock: stock,
|
||||
qty_actual: scannedMap.value.get(foundItem.uuid || foundItem.sku) || 0,
|
||||
scanned: true,
|
||||
uniqueKey: `${type}_${foundItem.id}`,
|
||||
source_table: typeToSourceTable(type),
|
||||
stock_id: foundItem.id
|
||||
}
|
||||
|
||||
openQtyDialog(item)
|
||||
} else {
|
||||
ElMessage.error(`不在库条码: ${trimCode}`)
|
||||
if (navigator.vibrate) navigator.vibrate([200, 50, 200])
|
||||
} catch (e) {
|
||||
ElMessage.error('查询库存失败')
|
||||
console.error(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@ -846,21 +760,61 @@ const closeScanner = () => {
|
||||
showCamera.value = false
|
||||
}
|
||||
|
||||
// 手动输入条码
|
||||
// 手动输入条码 - 实时查询后端
|
||||
const handleManualInput = async () => {
|
||||
const code = barcodeInput.value.trim()
|
||||
if (!code) return
|
||||
|
||||
if (code.length < 3) {
|
||||
ElMessage.warning('输入内容过短,请输入完整条码')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const item = allData.value.find(i => i.uuid === code || i.bar_code === code)
|
||||
const res: any = await getStockList({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
keyword: code
|
||||
})
|
||||
|
||||
if (item) {
|
||||
if (navigator.vibrate) navigator.vibrate(100)
|
||||
openQtyDialog(item)
|
||||
} else {
|
||||
ElMessage.error(`不在库条码: ${code}`)
|
||||
if (!res || !res.data || !res.data.list || res.data.list.length === 0) {
|
||||
ElMessage.error(`未找到该物料库存: ${code}`)
|
||||
return
|
||||
}
|
||||
|
||||
const foundItem = res.data.list.find((i: any) =>
|
||||
i.uuid === code || i.sku === code || i.barcode === code || i.bar_code === code
|
||||
)
|
||||
|
||||
if (!foundItem) {
|
||||
ElMessage.error(`未找到该物料库存: ${code}`)
|
||||
return
|
||||
}
|
||||
|
||||
if (navigator.vibrate) navigator.vibrate(100)
|
||||
|
||||
const stock = parseFloat(foundItem.stock_quantity || foundItem.qty_stock || 0)
|
||||
const type = foundItem.stock_type || foundItem.type || 'material'
|
||||
const item: StockItem = {
|
||||
...foundItem,
|
||||
name: foundItem.name || foundItem.material_name || foundItem.product_name || '未知物品',
|
||||
standard: foundItem.spec_model || foundItem.standard || foundItem.model || '',
|
||||
sku: foundItem.sku || '',
|
||||
uuid: foundItem.uuid || foundItem.sku || '',
|
||||
bar_code: foundItem.bar_code || foundItem.barcode || '',
|
||||
qty_stock: stock,
|
||||
qty_actual: scannedMap.value.get(foundItem.uuid || foundItem.sku) || 0,
|
||||
scanned: true,
|
||||
uniqueKey: `${type}_${foundItem.id}`,
|
||||
source_table: typeToSourceTable(type),
|
||||
stock_id: foundItem.id
|
||||
}
|
||||
|
||||
openQtyDialog(item)
|
||||
} catch (e) {
|
||||
ElMessage.error('查询库存失败')
|
||||
console.error(e)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@ -880,23 +834,25 @@ const openQtyDialog = (item: StockItem) => {
|
||||
const handleManualConfirm = () => {
|
||||
if (!currentItem.value) return
|
||||
const val = inputQty.value === undefined ? 0 : inputQty.value
|
||||
const remark = inputRemark.value
|
||||
|
||||
// ★★★ 乐观更新:立即本地更新 UI,不等待后端响应 ★★★
|
||||
// ★★★ 更新已扫码物料列表 ★★★
|
||||
currentItem.value.scanned = true
|
||||
currentItem.value.qty_actual = val
|
||||
scannedMap.value.set(currentItem.value.uuid, val)
|
||||
|
||||
// 保存备注用于异步提交
|
||||
const remark = inputRemark.value
|
||||
// 检查是否已存在于 tableData,如果存在则更新,否则添加
|
||||
const existingIndex = tableData.value.findIndex(i => i.uuid === currentItem.value!.uuid)
|
||||
if (existingIndex >= 0) {
|
||||
tableData.value[existingIndex] = { ...currentItem.value }
|
||||
} else {
|
||||
tableData.value.push({ ...currentItem.value })
|
||||
}
|
||||
|
||||
showQtyDialog.value = false
|
||||
// 重置备注
|
||||
inputRemark.value = ''
|
||||
ElMessage.success(`已记录实盘: ${val}`)
|
||||
|
||||
// ★★★ 取消自动弹摄像头,把开启摄像头的控制权完全交还给用户
|
||||
// 用户需主动点击才能开启摄像头
|
||||
|
||||
// ★★★ 异步保存到后端,不阻塞 UI(fire-and-forget)★★★
|
||||
syncToBackend(currentItem.value.uuid, val, remark)
|
||||
}
|
||||
@ -914,10 +870,17 @@ const syncToBackend = (uuid: string, quantity: number, remark: string) => {
|
||||
}
|
||||
|
||||
const updateAndSync = async (item: StockItem, quantity: number, remark: string = '') => {
|
||||
// ★★★ 保留原有逻辑用于兼容性,但不再使用 ★★★
|
||||
item.scanned = true
|
||||
item.qty_actual = quantity
|
||||
scannedMap.value.set(item.uuid, quantity)
|
||||
|
||||
// 更新 tableData
|
||||
const existingIndex = tableData.value.findIndex(i => i.uuid === item.uuid)
|
||||
if (existingIndex >= 0) {
|
||||
tableData.value[existingIndex] = { ...item }
|
||||
} else {
|
||||
tableData.value.push({ ...item })
|
||||
}
|
||||
}
|
||||
|
||||
const closeOverlays = () => {
|
||||
@ -961,34 +924,17 @@ const exportToExcel = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const filteredList = computed(() => {
|
||||
let result = allData.value
|
||||
if (filterType.value === 'scanned') result = result.filter(i => i.scanned)
|
||||
else if (filterType.value === 'missing') result = result.filter(i => !i.scanned)
|
||||
|
||||
if (searchKeyword.value) {
|
||||
const kw = searchKeyword.value.toLowerCase()
|
||||
result = result.filter(i =>
|
||||
i.name.toLowerCase().includes(kw) ||
|
||||
i.uuid.includes(kw) ||
|
||||
(i.sku && i.sku.toLowerCase().includes(kw)) ||
|
||||
(i.batch_no && i.batch_no.toLowerCase().includes(kw))
|
||||
)
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
const stats = computed(() => {
|
||||
const total = allData.value.length
|
||||
const scanned = allData.value.filter(i => i.scanned).length
|
||||
const varianceItems = allData.value.filter(i =>
|
||||
const total = tableData.value.length
|
||||
const scanned = tableData.value.filter(i => i.scanned).length
|
||||
const varianceItems = tableData.value.filter(i =>
|
||||
!i.scanned || (i.scanned && i.qty_actual !== parseFloat(i.qty_stock as any))
|
||||
).length
|
||||
return { total, scanned, varianceItems }
|
||||
})
|
||||
|
||||
const varianceList = computed(() => {
|
||||
return allData.value
|
||||
return tableData.value
|
||||
.filter(i => !i.scanned || (i.scanned && i.qty_actual !== parseFloat(i.qty_stock as any)))
|
||||
.map(i => ({
|
||||
...i,
|
||||
|
||||
Reference in New Issue
Block a user