feat: 盘点合并列表 — 服务端JOIN+DB分页,消除99999全量加载

## stock.py
- 新增 GET /draft/merged-list: UNION ALL三表+LEFT JOIN draft
  数据库级 LIMIT/OFFSET 分页,不再加载全量到Python内存
- 参数: session_id, keyword, status_filter, page, pageSize
- 返回: list, total, total_scanned (已扫去重数)

## stock.ts
- 新增 getDraftMergedList() API函数

## stocktake/index.vue
- fetchInventoryList: 改用merged-list单次调用替代99999全量+find()
- resumeSession: limit 99999→1 先检查存在,再limit 500加载
This commit is contained in:
yueli
2026-07-16 13:08:29 +08:00
parent e313f575bf
commit 8c10bb0903
3 changed files with 168 additions and 68 deletions

View File

@ -73,6 +73,21 @@ export function getBom(parentId: number) {
})
}
// 盘点物资合并列表(服务端 JOIN draft + stock替代前端全量加载+find
export function getDraftMergedList(params: {
session_id: string
keyword?: string
status_filter?: string
page?: number
pageSize?: number
}) {
return request({
url: '/v1/inbound/stock/draft/merged-list',
method: 'get',
params
})
}
// 获取应盘物资清单(盘点基数)
export function getAllStocktakeItems(params?: { keyword?: string; session_id?: string }) {
return request({

View File

@ -413,7 +413,7 @@
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
import { getStockList, getAllStocktakeItems, updateStocktakeQuantity } from '@/api/inbound/stock'
import { getStockList, getAllStocktakeItems, getDraftMergedList, updateStocktakeQuantity } from '@/api/inbound/stock'
import QrScanner from '@/components/QrScanner/index.vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, VideoPlay, VideoPause, List, Checked, Download, ArrowRight, Cloudy, Edit, EditPen, CameraFilled, Close, WarningFilled } from '@element-plus/icons-vue'
@ -654,19 +654,28 @@ const resumeSession = async () => {
try {
// 获取最新的会话(后端已移除 is_finished 字段)
// 后端返回格式已改为 { items: [], total: number }
// ★ 仅检查是否有草稿存在取1条即可无需99999全量
const res: any = await request({
url: '/v1/inbound/stock/draft/list',
method: 'get',
params: { page: 1, limit: 99999 } // ★ 获取全量草稿数据
params: { page: 1, limit: 1 }
})
const drafts = res && res.items ? res.items : []
const total = res?.total || 0
if (!drafts || drafts.length === 0) {
if (total === 0) {
ElMessage.warning('没有找到未完成的盘点记录')
return
}
// ★ 获取实际草稿数据用于恢复分页加载单次最多500条
const fullRes = await request({
url: '/v1/inbound/stock/draft/list',
method: 'get',
params: { page: 1, limit: 500 }
})
const drafts = fullRes?.items || []
// 恢复已扫描的数据 - 简化为直接激活会话
// 从草稿中获取 session_id
const sessionIds = [...new Set(drafts.map((d: any) => d.session_id))]
@ -975,75 +984,22 @@ const filteredVarianceList = computed(() => {
})
// ★ 新增: 获取盘点清单数据(合并全量应盘物资 + 已盘点记录)
// silent: 是否静默模式(不显示 loading不报错
// ★ 使用服务端 merged-list 端点(替代 99999 全量 + 前端 find()
const fetchInventoryList = async (silent = false) => {
if (!silent) listLoading.value = true
try {
// 1. 获取已盘点记录
const res: any = await request({
url: '/v1/inbound/stock/draft/list',
method: 'get',
params: {
page: 1,
limit: 99999, // ★ 获取全量草稿数据
keyword: listKeyword.value,
session_id: currentSessionId.value // ★ 必须传递 session_id 隔离会话
}
const res: any = await getDraftMergedList({
session_id: currentSessionId.value,
keyword: listKeyword.value || undefined,
status_filter: listStatusFilter.value || undefined,
page: listPage.value,
pageSize: listLimit.value
})
const scannedDrafts = res?.items || []
// ★ 使用返回的 total 获取真实已盘数量,而不是受限的数组长度
const totalScanned = res?.total || scannedDrafts.length
// 保存全量草稿记录用于全局统计
allScannedDrafts.value = scannedDrafts
// 直接读取后端算好的去重已盘数
totalScannedCount.value = res?.total_scanned || 0
// 2. 使用全量应盘物资列表
// 对于每个应盘物资,检查是否有对应的盘点记录
let mergedData = allStockItems.value.map(item => {
// 查找对应的盘点记录
const draft = scannedDrafts.find((d: any) =>
d.source_table === item.source_table && d.stock_id === item.id
)
return {
id: draft?.id || null,
stock_id: item.id,
source_table: item.source_table,
uniqueKey: `${item.source_table}_${item.id}`, // ★ 绝对唯一键解决row-key冲突
sku: item.sku,
material_name: item.material_name,
spec_model: item.spec_model,
stock_qty: item.stock_qty, // 账面数(盲盘时隐藏)
quantity: draft?.quantity ?? draft?.qty_actual ?? 0, // 兼容后端字段名
diff_qty: draft ? (draft.quantity - item.stock_qty) : -item.stock_qty, // 差异
remark: draft?.remark || '',
warehouse_location: item.warehouse_location
}
})
// 3. 关键词过滤
if (listKeyword.value) {
const kw = listKeyword.value.toLowerCase()
mergedData = mergedData.filter((item: any) =>
item.sku?.toLowerCase().includes(kw) ||
item.material_name?.toLowerCase().includes(kw)
)
}
// 4. 状态过滤
if (listStatusFilter.value === 'counted') {
mergedData = mergedData.filter((item: any) => item.quantity > 0)
} else if (listStatusFilter.value === 'uncounted') {
mergedData = mergedData.filter((item: any) => !item.quantity || item.quantity === 0)
}
// 5. 分页
listTotalFiltered.value = mergedData.length
const start = (listPage.value - 1) * listLimit.value
listData.value = mergedData.slice(start, start + listLimit.value)
const data = res?.data || {}
listData.value = data.list || []
listTotalFiltered.value = data.total || 0
totalScannedCount.value = data.total_scanned || 0
} catch (e) {
if (!silent) ElMessage.error('获取盘点清单失败')