diff --git a/inventory-web/src/views/stock/stocktake/index.vue b/inventory-web/src/views/stock/stocktake/index.vue
index f6a5e98..37a6f3b 100644
--- a/inventory-web/src/views/stock/stocktake/index.vue
+++ b/inventory-web/src/views/stock/stocktake/index.vue
@@ -153,6 +153,26 @@
+
+
+
+ 最近扫码
+ 最新 {{ recentScans.length }} 条
+
+
+ -
+ {{ r.time }}
+ {{ r.name }}
+ {{ r.spec }}
+ ×{{ r.quantity }}
+
+
+
+
@@ -537,17 +557,27 @@ const searchSku = ref('')
// ★ 新增: 盘点清单弹窗分页
const listPage = ref(1)
const listLimit = ref(20)
-const listTotal = ref(0)
const listKeyword = ref('')
const listLoading = ref(false)
const listData = ref([])
const listStatusFilter = ref<'all' | 'counted' | 'uncounted'>('all')
const allStockItems = ref([]) // 全量应盘物资(盘点基数)
const totalStockCount = ref(0) // ★ 全量应盘物资总数(不受limit限制)
-const totalScannedCount = ref(0) // ★ 后端去重的真实已盘数量
-const allScannedDrafts = ref([]) // 全量草稿记录(脱离分页和过滤)
+// ★ 后端去重的真实已盘数量。唯一写入点是 fetchInventoryList(merged-list),
+// 见下方注释 —— 不要再从 all-items 写入,避免两个接口各自为政
+const totalScannedCount = ref(0)
const listTotalFiltered = ref(0) // 过滤后的总数
+// ★ 最近扫码记录:平板大屏给工人的即时反馈,扫完瞥一眼就能确认有没有扫错/漏扫
+const RECENT_SCAN_LIMIT = 10
+const recentScans = ref>([])
+
// ★ 新增: 会话ID
const currentSessionId = ref('')
@@ -637,7 +667,8 @@ const fetchAllStockItems = async (page = 1) => {
allStockItems.value = res.data.items || []
// ★ 使用返回的 total 获取真实总数,而不是数组长度
totalStockCount.value = res.data.total || allStockItems.value.length
- totalScannedCount.value = res.data.total_scanned || 0
+ // 注意:这里刻意不再写 totalScannedCount。
+ // 该值统一以 merged-list(fetchInventoryList)为准,避免两个接口双写。
}
} catch (e) {
console.error('获取应盘物资清单失败', e)
@@ -805,6 +836,8 @@ const checkServerDraft = async () => {
const enterScanning = async (message: string) => {
isSessionActive.value = true
localStorage.setItem('stocktake_phase', 'scanning')
+ // 最近扫码流水属于「本次作业」,换会话/重新加入都要清空,避免看到上一个会话的残留
+ recentScans.value = []
await fetchAllStockItems()
await fetchInventoryList()
ElMessage.success(message)
@@ -1099,29 +1132,56 @@ const handleManualConfirm = () => {
if (!currentItem.value) return
const val = inputQty.value === undefined ? 0 : inputQty.value
const remark = inputRemark.value
+ const item = currentItem.value
- // ★★★ 直接保存到后端,不使用本地缓存 ★★★
+ // 先收起弹窗;成功提示延后到落库确认之后(不再乐观提示)
showQtyDialog.value = false
inputRemark.value = ''
- ElMessage.success(`已记录实盘: ${val}`)
- // ★★★ 异步保存到后端,不阻塞 UI(fire-and-forget)★★★
- syncToBackend(currentItem.value.uuid, val, remark)
+ syncToBackend(item, val, remark)
}
-// ★★★ 乐观更新:异步保存到后端,不阻塞 UI ★★★
-const syncToBackend = (uuid: string, quantity: number, remark: string) => {
+// ★ 落库成功后才记入最近流水
+const pushRecentScan = (item: StockItem, quantity: number) => {
+ recentScans.value.unshift({
+ uuid: item.uuid,
+ name: item.name,
+ spec: item.standard || '',
+ quantity,
+ time: new Date().toLocaleTimeString('zh-CN', { hour12: false })
+ })
+ if (recentScans.value.length > RECENT_SCAN_LIMIT) {
+ recentScans.value.length = RECENT_SCAN_LIMIT
+ }
+}
+
+// ★ 保存到后端并等待确认。
+// 不再「乐观更新」:只有 addDraft 真正 resolve 后才提示成功、记流水、刷统计。
+// 否则请求失败时工人已看到「已记录」,会以为扫进去了,而这条实盘记录其实丢了。
+const syncToBackend = (item: StockItem, quantity: number, remark: string) => {
+ const uuid = item.uuid
syncStatus.value = 'syncing'
api.addDraft({ uuid, quantity, remark })
.then(() => {
syncStatus.value = 'success'
+ ElMessage.success(`已录入:${item.name} × ${quantity}`)
+ pushRecentScan(item, quantity)
// 静默刷新统计数字
fetchInventoryList(true)
// ★ 扫码成功:该物品置顶到当前视图第一行并高亮
pinScannedItem(uuid)
})
- .catch(() => {
+ .catch((e: any) => {
syncStatus.value = 'failed'
+ // ★ 这一笔确实没进库,且系统不会自动重试 —— 必须让工人看到并手动重扫。
+ // 停留久一点 + 给关闭按钮,避免连续扫码时提示被后续消息挤掉。
+ ElMessage({
+ type: 'error',
+ duration: 10000,
+ showClose: true,
+ message: `网络异常,上一笔条码 [${uuid}] 录入失败,请重试!`
+ })
+ console.error('扫码落库失败', e)
})
}
@@ -1518,6 +1578,54 @@ const goToVarianceReview = () => {
.stat-card.error .stat-val { color: #f56c6c; }
.stat-arrow { width: 20px; color: #c0c4cc; }
+/* ★ 最近扫码记录:平板大屏用,行高放大便于一瞥确认 */
+.recent-scans {
+ background: #fff;
+ border: 1px solid #ebeef5;
+ border-radius: 8px;
+ padding: 10px 12px;
+ margin-bottom: 20px;
+ flex-shrink: 0;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.05);
+}
+.recent-scans-title {
+ display: flex;
+ justify-content: space-between;
+ align-items: baseline;
+ font-size: 13px;
+ font-weight: 600;
+ color: #303133;
+ margin-bottom: 6px;
+}
+.recent-scans-hint { font-size: 11px; font-weight: 400; color: #909399; }
+.recent-scans-list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ max-height: 220px;
+ overflow-y: auto;
+}
+.recent-scans-list li {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 6px 8px;
+ border-radius: 4px;
+ font-size: 14px;
+ line-height: 1.4;
+}
+.recent-scans-list li:nth-child(odd) { background: #fafafa; }
+/* 最新一条高亮,扫完立刻能对上 */
+.recent-scans-list li.is-latest {
+ background: #f0f9eb;
+ border-left: 3px solid #67c23a;
+ font-weight: 600;
+}
+.rs-time { flex: 0 0 auto; font-size: 12px; color: #909399; font-variant-numeric: tabular-nums; }
+.rs-name { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #303133; }
+.rs-spec { flex: 0 1 auto; max-width: 30%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; color: #909399; }
+.rs-qty { flex: 0 0 auto; font-weight: 700; color: #67c23a; font-variant-numeric: tabular-nums; }
+
.w-100 { width: 100%; }
.action-btn { font-weight: bold; height: 48px; }