diff --git a/inventory-web/src/views/stock/stocktake/index.vue b/inventory-web/src/views/stock/stocktake/index.vue index b01338f..6643183 100644 --- a/inventory-web/src/views/stock/stocktake/index.vue +++ b/inventory-web/src/views/stock/stocktake/index.vue @@ -456,88 +456,21 @@ let countdownTimer: any = null // ★ 新增: 防呆确认弹窗显示状态 const showConfirmDialog = ref(false) -const allData = ref([]) +// ★★★ 核心修改:只存储已扫码的物料列表,不再缓存全量库存 ★★★ +const tableData = ref([]) const scannedMap = ref>(new Map()) const borrowedQuantities = ref>({}) -// ★ 新增: 分页查询参数 -const queryParams = reactive({ - page: 1, - limit: 20, - keyword: '' -}) -const total = ref(0) - // ★ 新增: 会话ID const currentSessionId = ref('') const varianceLoading = ref(false) -const filterType = ref('all') -const searchKeyword = ref('') - const currentItem = ref(null) const inputQty = ref(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,