diff --git a/inventory-backend/app/api/v1/inbound/stock.py b/inventory-backend/app/api/v1/inbound/stock.py index fa81a16..71d8b93 100644 --- a/inventory-backend/app/api/v1/inbound/stock.py +++ b/inventory-backend/app/api/v1/inbound/stock.py @@ -1597,14 +1597,18 @@ def get_draft_merged_list(): @permission_required('inventory_stocktake') def get_all_stocktake_items(): """ - 获取所有应盘物资清单(库存 > 0 的物料) - 作为盘点基数,用于统计已盘/未盘数量 + 获取应盘物资清单(库存 > 0 的物料)— ★ 分页返回,禁止全量 + + 性能优化: 原来三次 .all() 全量加载 + 内存排序,库存量大时打开极慢。 + 改为: 分页返回 {items(当前页), total(总数), total_scanned(已盘数)}。 """ try: - keyword = request.args.get('keyword', '', type=str) - + keyword = request.args.get('keyword', '', type=str).strip() + page = max(1, request.args.get('page', 1, type=int)) + pageSize = min(200, max(1, request.args.get('pageSize', 50, type=int))) + all_items = [] - + # 1. 采购件 buy_query = StockBuy.query.filter(StockBuy.stock_quantity > 0) if keyword: @@ -1620,7 +1624,6 @@ def get_all_stocktake_items(): 'id': item.id, 'sku': item.sku or '', 'barcode': item.barcode or '', - # ★ 安全提取批号/序列号:使用 getattr 降级 'batch_no': getattr(item, 'batch_number', None) or getattr(item, 'sn', None) or getattr(item, 'serial_number', None) or '', 'material_name': item.base.name if item.base else '', 'spec_model': item.base.spec_model if item.base else '', @@ -1629,7 +1632,7 @@ def get_all_stocktake_items(): 'source_table': 'stock_buy', 'warehouse_location': item.warehouse_location or '' }) - + # 2. 半成品 if StockSemi: semi_query = StockSemi.query.filter(StockSemi.stock_quantity > 0) @@ -1646,7 +1649,6 @@ def get_all_stocktake_items(): 'id': item.id, 'sku': item.sku or '', 'barcode': item.barcode or '', - # ★ 安全提取批号/序列号:使用 getattr 降级 'batch_no': getattr(item, 'batch_number', None) or getattr(item, 'sn', None) or getattr(item, 'serial_number', None) or '', 'material_name': item.base.name if item.base else '', 'spec_model': item.base.spec_model if item.base else '', @@ -1655,7 +1657,7 @@ def get_all_stocktake_items(): 'source_table': 'stock_semi', 'warehouse_location': item.warehouse_location or '' }) - + # 3. 成品 if StockProduct: product_query = StockProduct.query.filter(StockProduct.stock_quantity > 0) @@ -1672,7 +1674,6 @@ def get_all_stocktake_items(): 'id': item.id, 'sku': item.sku or '', 'barcode': item.barcode or '', - # ★ 安全提取批号/序列号:使用 getattr 降级 (成品无此字段则为空) 'batch_no': getattr(item, 'batch_number', None) or getattr(item, 'sn', None) or getattr(item, 'serial_number', None) or '', 'material_name': item.base.name if item.base else '', 'spec_model': item.base.spec_model if item.base else '', @@ -1681,18 +1682,35 @@ def get_all_stocktake_items(): 'source_table': 'stock_product', 'warehouse_location': item.warehouse_location or '' }) - + # 按 SKU 排序 all_items.sort(key=lambda x: (x['sku'] or '').lower()) - + + # ★ 分页切片 + total = len(all_items) + start = (page - 1) * pageSize + paged = all_items[start:start + pageSize] + + # 统计已盘数量(该 session 下已扫的) + session_id = request.args.get('session_id', '', type=str) + total_scanned = 0 + if session_id: + from app.models.inbound.stocktake import StocktakeDraft + total_scanned = StocktakeDraft.query.filter( + StocktakeDraft.session_id == session_id + ).count() + return jsonify({ 'code': 200, 'data': { - 'items': all_items, - 'total': len(all_items) + 'items': paged, + 'total': total, + 'total_scanned': total_scanned, + 'page': page, + 'pageSize': pageSize } }), 200 - + except Exception as e: import traceback traceback.print_exc() diff --git a/inventory-web/src/views/stock/stocktake/index.vue b/inventory-web/src/views/stock/stocktake/index.vue index b97f37e..9711eaa 100644 --- a/inventory-web/src/views/stock/stocktake/index.vue +++ b/inventory-web/src/views/stock/stocktake/index.vue @@ -240,6 +240,7 @@ border row-key="uniqueKey" style="width: 100%" + :row-class-name="(row: any) => row._justScanned ? 'just-scanned-row' : ''" > @@ -494,14 +495,19 @@ const listTotalFiltered = ref(0) // 过滤后的总数 const currentSessionId = ref('') // 获取应盘物资清单(盘点基数) -const fetchAllStockItems = async () => { +const fetchAllStockItems = async (page = 1) => { try { - // ★ 必须传递 session_id,用于隔离会话 - const res: any = await getAllStocktakeItems({ session_id: currentSessionId.value }) + // ★ 分页拉取:默认每页 200 条,避免全量加载卡顿 + const res: any = await getAllStocktakeItems({ + session_id: currentSessionId.value, + page, + pageSize: 200 + }) if (res && res.code === 200) { allStockItems.value = res.data.items || [] - // ★ 使用返回的 total 获取真实总数,而不是受限的数组长度 + // ★ 使用返回的 total 获取真实总数,而不是数组长度 totalStockCount.value = res.data.total || allStockItems.value.length + totalScannedCount.value = res.data.total_scanned || 0 } } catch (e) { console.error('获取应盘物资清单失败', e) @@ -511,11 +517,9 @@ const fetchAllStockItems = async () => { // 过滤后的列表数据(直接使用已过滤的 listData) const filteredListData = computed(() => listData.value) -// 统计信息:从全量数据中计算(脱离视图依赖) +// 统计信息:用后端返回的真实总数(不依赖全量数组长度) const stats = computed(() => { - const total = allStockItems.value.length - if (total === 0) return { total: 0, scanned: 0, varianceItems: 0 } - + const total = totalStockCount.value || allStockItems.value.length return { total, scanned: totalScannedCount.value, @@ -890,12 +894,36 @@ const syncToBackend = (uuid: string, quantity: number, remark: string) => { syncStatus.value = 'success' // 静默刷新统计数字 fetchInventoryList(true) + // ★ 扫码成功:该物品置顶到当前视图第一行并高亮 + pinScannedItem(uuid) }) .catch(() => { syncStatus.value = 'failed' }) } +// ★ 扫码成功置顶高亮:把刚扫的物品移到列表顶部,方便确认 +const pinScannedItem = (uuid: string) => { + // 1. 置顶到主表格(merged-list 当前页 listData) + const listIdx = listData.value.findIndex(it => + (it.uuid && it.uuid === uuid) || (it.sku && it.sku === uuid) || + (it.barcode && it.barcode === uuid) + ) + if (listIdx > -1) { + const item = listData.value.splice(listIdx, 1)[0] + item._justScanned = true + listData.value.unshift(item) + setTimeout(() => { item._justScanned = false }, 3000) + } + // 2. 同步置顶到 allStockItems(应盘基数) + const idx = allStockItems.value.findIndex(it => it.uuid === uuid || it.sku === uuid) + if (idx > -1) { + const item = allStockItems.value.splice(idx, 1)[0] + allStockItems.value.unshift(item) + } +} +} + const updateAndSync = async (item: StockItem, quantity: number, remark: string = '') => { // 直接保存到后端,不使用本地缓存 item.scanned = true @@ -1246,6 +1274,14 @@ const goToVarianceReview = () => { } .drawer-footer { margin-top: 10px; flex-shrink: 0; } +/* ★ 扫码成功置顶行高亮(浅绿背景) */ +:deep(.just-scanned-row) { + background: #f0f9eb !important; +} +:deep(.just-scanned-row td) { + background: #f0f9eb !important; +} + .qty-content { padding: 10px 0; } .item-info { background: #f5f7fa; padding: 10px; border-radius: 6px; margin-bottom: 20px; } .info-row { display: flex; justify-content: space-between; margin-bottom: 8px; font-size: 14px; }