perf(stocktake): 应盘清单分页 + 扫码置顶高亮
性能修复(解决打开费劲):
- get_all_stocktake_items 改为分页返回 {items(当前页), total, total_scanned}
不再三次 .all() 全量加载 + 内存排序
- 前端 fetchAllStockItems 分页拉取(每页200),stats 用后端返回的 total
不依赖全量数组长度
体验优化:
- 扫码成功后将物品置顶到当前表格第一行并浅绿高亮3秒
库管无需翻页找刚扫的物品,'嘀'一下确认数量即可继续
- 主表格行高亮样式 .just-scanned-row
This commit is contained in:
@ -1597,14 +1597,18 @@ def get_draft_merged_list():
|
|||||||
@permission_required('inventory_stocktake')
|
@permission_required('inventory_stocktake')
|
||||||
def get_all_stocktake_items():
|
def get_all_stocktake_items():
|
||||||
"""
|
"""
|
||||||
获取所有应盘物资清单(库存 > 0 的物料)
|
获取应盘物资清单(库存 > 0 的物料)— ★ 分页返回,禁止全量
|
||||||
作为盘点基数,用于统计已盘/未盘数量
|
|
||||||
|
性能优化: 原来三次 .all() 全量加载 + 内存排序,库存量大时打开极慢。
|
||||||
|
改为: 分页返回 {items(当前页), total(总数), total_scanned(已盘数)}。
|
||||||
"""
|
"""
|
||||||
try:
|
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 = []
|
all_items = []
|
||||||
|
|
||||||
# 1. 采购件
|
# 1. 采购件
|
||||||
buy_query = StockBuy.query.filter(StockBuy.stock_quantity > 0)
|
buy_query = StockBuy.query.filter(StockBuy.stock_quantity > 0)
|
||||||
if keyword:
|
if keyword:
|
||||||
@ -1620,7 +1624,6 @@ def get_all_stocktake_items():
|
|||||||
'id': item.id,
|
'id': item.id,
|
||||||
'sku': item.sku or '',
|
'sku': item.sku or '',
|
||||||
'barcode': item.barcode 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 '',
|
'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 '',
|
'material_name': item.base.name if item.base else '',
|
||||||
'spec_model': item.base.spec_model 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',
|
'source_table': 'stock_buy',
|
||||||
'warehouse_location': item.warehouse_location or ''
|
'warehouse_location': item.warehouse_location or ''
|
||||||
})
|
})
|
||||||
|
|
||||||
# 2. 半成品
|
# 2. 半成品
|
||||||
if StockSemi:
|
if StockSemi:
|
||||||
semi_query = StockSemi.query.filter(StockSemi.stock_quantity > 0)
|
semi_query = StockSemi.query.filter(StockSemi.stock_quantity > 0)
|
||||||
@ -1646,7 +1649,6 @@ def get_all_stocktake_items():
|
|||||||
'id': item.id,
|
'id': item.id,
|
||||||
'sku': item.sku or '',
|
'sku': item.sku or '',
|
||||||
'barcode': item.barcode 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 '',
|
'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 '',
|
'material_name': item.base.name if item.base else '',
|
||||||
'spec_model': item.base.spec_model 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',
|
'source_table': 'stock_semi',
|
||||||
'warehouse_location': item.warehouse_location or ''
|
'warehouse_location': item.warehouse_location or ''
|
||||||
})
|
})
|
||||||
|
|
||||||
# 3. 成品
|
# 3. 成品
|
||||||
if StockProduct:
|
if StockProduct:
|
||||||
product_query = StockProduct.query.filter(StockProduct.stock_quantity > 0)
|
product_query = StockProduct.query.filter(StockProduct.stock_quantity > 0)
|
||||||
@ -1672,7 +1674,6 @@ def get_all_stocktake_items():
|
|||||||
'id': item.id,
|
'id': item.id,
|
||||||
'sku': item.sku or '',
|
'sku': item.sku or '',
|
||||||
'barcode': item.barcode 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 '',
|
'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 '',
|
'material_name': item.base.name if item.base else '',
|
||||||
'spec_model': item.base.spec_model 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',
|
'source_table': 'stock_product',
|
||||||
'warehouse_location': item.warehouse_location or ''
|
'warehouse_location': item.warehouse_location or ''
|
||||||
})
|
})
|
||||||
|
|
||||||
# 按 SKU 排序
|
# 按 SKU 排序
|
||||||
all_items.sort(key=lambda x: (x['sku'] or '').lower())
|
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({
|
return jsonify({
|
||||||
'code': 200,
|
'code': 200,
|
||||||
'data': {
|
'data': {
|
||||||
'items': all_items,
|
'items': paged,
|
||||||
'total': len(all_items)
|
'total': total,
|
||||||
|
'total_scanned': total_scanned,
|
||||||
|
'page': page,
|
||||||
|
'pageSize': pageSize
|
||||||
}
|
}
|
||||||
}), 200
|
}), 200
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|||||||
@ -240,6 +240,7 @@
|
|||||||
border
|
border
|
||||||
row-key="uniqueKey"
|
row-key="uniqueKey"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
|
:row-class-name="(row: any) => row._justScanned ? 'just-scanned-row' : ''"
|
||||||
>
|
>
|
||||||
<el-table-column prop="sku" label="SKU" width="140" show-overflow-tooltip />
|
<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="material_name" label="名称" min-width="120" show-overflow-tooltip />
|
||||||
@ -494,14 +495,19 @@ const listTotalFiltered = ref(0) // 过滤后的总数
|
|||||||
const currentSessionId = ref<string>('')
|
const currentSessionId = ref<string>('')
|
||||||
|
|
||||||
// 获取应盘物资清单(盘点基数)
|
// 获取应盘物资清单(盘点基数)
|
||||||
const fetchAllStockItems = async () => {
|
const fetchAllStockItems = async (page = 1) => {
|
||||||
try {
|
try {
|
||||||
// ★ 必须传递 session_id,用于隔离会话
|
// ★ 分页拉取:默认每页 200 条,避免全量加载卡顿
|
||||||
const res: any = await getAllStocktakeItems({ session_id: currentSessionId.value })
|
const res: any = await getAllStocktakeItems({
|
||||||
|
session_id: currentSessionId.value,
|
||||||
|
page,
|
||||||
|
pageSize: 200
|
||||||
|
})
|
||||||
if (res && res.code === 200) {
|
if (res && res.code === 200) {
|
||||||
allStockItems.value = res.data.items || []
|
allStockItems.value = res.data.items || []
|
||||||
// ★ 使用返回的 total 获取真实总数,而不是受限的数组长度
|
// ★ 使用返回的 total 获取真实总数,而不是数组长度
|
||||||
totalStockCount.value = res.data.total || allStockItems.value.length
|
totalStockCount.value = res.data.total || allStockItems.value.length
|
||||||
|
totalScannedCount.value = res.data.total_scanned || 0
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('获取应盘物资清单失败', e)
|
console.error('获取应盘物资清单失败', e)
|
||||||
@ -511,11 +517,9 @@ const fetchAllStockItems = async () => {
|
|||||||
// 过滤后的列表数据(直接使用已过滤的 listData)
|
// 过滤后的列表数据(直接使用已过滤的 listData)
|
||||||
const filteredListData = computed(() => listData.value)
|
const filteredListData = computed(() => listData.value)
|
||||||
|
|
||||||
// 统计信息:从全量数据中计算(脱离视图依赖)
|
// 统计信息:用后端返回的真实总数(不依赖全量数组长度)
|
||||||
const stats = computed(() => {
|
const stats = computed(() => {
|
||||||
const total = allStockItems.value.length
|
const total = totalStockCount.value || allStockItems.value.length
|
||||||
if (total === 0) return { total: 0, scanned: 0, varianceItems: 0 }
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
total,
|
total,
|
||||||
scanned: totalScannedCount.value,
|
scanned: totalScannedCount.value,
|
||||||
@ -890,12 +894,36 @@ const syncToBackend = (uuid: string, quantity: number, remark: string) => {
|
|||||||
syncStatus.value = 'success'
|
syncStatus.value = 'success'
|
||||||
// 静默刷新统计数字
|
// 静默刷新统计数字
|
||||||
fetchInventoryList(true)
|
fetchInventoryList(true)
|
||||||
|
// ★ 扫码成功:该物品置顶到当前视图第一行并高亮
|
||||||
|
pinScannedItem(uuid)
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
syncStatus.value = 'failed'
|
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 = '') => {
|
const updateAndSync = async (item: StockItem, quantity: number, remark: string = '') => {
|
||||||
// 直接保存到后端,不使用本地缓存
|
// 直接保存到后端,不使用本地缓存
|
||||||
item.scanned = true
|
item.scanned = true
|
||||||
@ -1246,6 +1274,14 @@ const goToVarianceReview = () => {
|
|||||||
}
|
}
|
||||||
.drawer-footer { margin-top: 10px; flex-shrink: 0; }
|
.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; }
|
.qty-content { padding: 10px 0; }
|
||||||
.item-info { background: #f5f7fa; padding: 10px; border-radius: 6px; margin-bottom: 20px; }
|
.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; }
|
.info-row { display: flex; justify-content: space-between; margin-bottom: 8px; font-size: 14px; }
|
||||||
|
|||||||
Reference in New Issue
Block a user