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; }