feat(stocktake): implement strict blind stocktake logic with hidden system qty, editable count and status filters
This commit is contained in:
@ -210,21 +210,30 @@
|
||||
|
||||
<el-drawer
|
||||
v-model="showList"
|
||||
title="📦 盘点清单 (点击修改)"
|
||||
title="📦 盘点清单"
|
||||
direction="btt"
|
||||
size="100%"
|
||||
destroy-on-close
|
||||
class="inventory-drawer"
|
||||
>
|
||||
<div class="drawer-layout" v-loading="listLoading">
|
||||
<div class="search-bar">
|
||||
<el-input v-model="listKeyword" placeholder="搜索 SKU..." :prefix-icon="Search" clearable @keyup.enter="handleListSearch" @clear="handleListSearch" style="width: 240px" />
|
||||
<!-- 搜索和状态筛选 -->
|
||||
<div class="search-bar" style="display: flex; align-items: center; gap: 10px; margin-bottom: 15px;">
|
||||
<el-input v-model="listKeyword" placeholder="搜索 SKU/名称..." :prefix-icon="Search" clearable @keyup.enter="handleListSearch" @clear="handleListSearch" style="width: 240px" />
|
||||
<el-button type="primary" @click="handleListSearch">搜索</el-button>
|
||||
<el-radio-group v-model="listStatusFilter" @change="handleListSearch" size="small">
|
||||
<el-radio-button value="all">全部</el-radio-button>
|
||||
<el-radio-button value="counted">已盘</el-radio-button>
|
||||
<el-radio-button value="uncounted">未盘</el-radio-button>
|
||||
</el-radio-group>
|
||||
<span style="margin-left: auto; color: #909399; font-size: 13px;">
|
||||
总品项: {{ stats.total }} | 已盘: {{ stats.scanned }} | 未盘: {{ stats.total - stats.scanned }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<el-table
|
||||
:data="listData"
|
||||
:data="filteredListData"
|
||||
height="100%"
|
||||
stripe
|
||||
border
|
||||
@ -234,16 +243,33 @@
|
||||
<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="spec_model" label="规格" width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="stock_qty" label="账面数" width="80" align="center" />
|
||||
<el-table-column prop="quantity" label="实盘数" width="80" align="center" />
|
||||
<el-table-column label="差异" width="80" align="center">
|
||||
<!-- 盲盘:隐藏账面数和差异列 -->
|
||||
<!-- <el-table-column prop="stock_qty" label="账面数" width="80" align="center" /> -->
|
||||
<el-table-column label="实盘数" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-input-number
|
||||
v-model="row.quantity"
|
||||
:min="0"
|
||||
:step="1"
|
||||
size="small"
|
||||
controls-position="right"
|
||||
@change="(val) => handleQuantityChange(row, val)"
|
||||
:disabled="!userStore.hasPermission('inventory_stocktake:operation')"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- <el-table-column label="差异" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<span :style="{ color: row.diff_qty > 0 ? '#67C23A' : row.diff_qty < 0 ? '#F56C6C' : '#909399' }">
|
||||
{{ row.diff_qty > 0 ? '+' : '' }}{{ row.diff_qty || 0 }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column> -->
|
||||
<el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<el-input v-model="row.remark" placeholder="备注" size="small" @blur="handleRemarkChange(row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
@ -253,7 +279,7 @@
|
||||
v-model:current-page="listPage"
|
||||
v-model:page-size="listLimit"
|
||||
:page-sizes="[20, 50, 100, 200]"
|
||||
:total="listTotal"
|
||||
:total="listTotalFiltered"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleListLimitChange"
|
||||
@current-change="handleListPageChange"
|
||||
@ -386,7 +412,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { getStockList } from '@/api/inbound/stock'
|
||||
import { getStockList, getAllStocktakeItems, 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'
|
||||
@ -448,22 +474,75 @@ const listTotal = ref(0)
|
||||
const listKeyword = ref('')
|
||||
const listLoading = ref(false)
|
||||
const listData = ref<any[]>([])
|
||||
|
||||
// ★ 新增: 盘点开始防呆倒计时
|
||||
const countdown = ref(0)
|
||||
let countdownTimer: any = null
|
||||
|
||||
// ★ 新增: 防呆确认弹窗显示状态
|
||||
const showConfirmDialog = ref(false)
|
||||
|
||||
// ★★★ 核心修改:只存储已扫码的物料列表,不再缓存全量库存 ★★★
|
||||
const tableData = ref<StockItem[]>([])
|
||||
const scannedMap = ref<Map<string, number>>(new Map())
|
||||
const borrowedQuantities = ref<Record<string, number>>({})
|
||||
const listStatusFilter = ref<'all' | 'counted' | 'uncounted'>('all')
|
||||
const allStockItems = ref<any[]>([]) // 全量应盘物资(盘点基数)
|
||||
const listTotalFiltered = ref(0) // 过滤后的总数
|
||||
|
||||
// ★ 新增: 会话ID
|
||||
const currentSessionId = ref<string>('')
|
||||
|
||||
// 获取应盘物资清单(盘点基数)
|
||||
const fetchAllStockItems = async () => {
|
||||
try {
|
||||
const res: any = await getAllStocktakeItems()
|
||||
if (res && res.code === 200) {
|
||||
allStockItems.value = res.data.items || []
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取应盘物资清单失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 过滤已在 fetchInventoryList 中处理,此处不再需要 computed
|
||||
// const filteredListData = computed(() => {
|
||||
// let items = [...listData.value]
|
||||
//
|
||||
// if (listStatusFilter.value === 'counted') {
|
||||
// items = items.filter(item => item.quantity > 0)
|
||||
// } else if (listStatusFilter.value === 'uncounted') {
|
||||
// items = items.filter(item => !item.quantity || item.quantity === 0)
|
||||
// }
|
||||
//
|
||||
// return items
|
||||
// })
|
||||
|
||||
// 统计信息:从全量应盘物资中计算
|
||||
const stats = computed(() => {
|
||||
// 已盘点数量:从已扫描的记录中获取
|
||||
const countedItems = new Set()
|
||||
listData.value.forEach(item => {
|
||||
if (item.stock_id && item.source_table) {
|
||||
countedItems.add(`${item.source_table}-${item.stock_id}`)
|
||||
}
|
||||
})
|
||||
|
||||
// 全量应盘物资中已盘的数量
|
||||
let scanned = 0
|
||||
allStockItems.value.forEach(item => {
|
||||
const key = `${item.source_table}-${item.id}`
|
||||
if (countedItems.has(key)) {
|
||||
scanned++
|
||||
}
|
||||
})
|
||||
|
||||
// 如果 allStockItems 为空,回退到旧的统计逻辑
|
||||
if (allStockItems.value.length === 0) {
|
||||
const total = listData.value.length
|
||||
const scannedCount = listData.value.filter(i => i.quantity > 0).length
|
||||
return {
|
||||
total,
|
||||
scanned: scannedCount,
|
||||
varianceItems: 0
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
total: allStockItems.value.length,
|
||||
scanned: scanned,
|
||||
varianceItems: 0
|
||||
}
|
||||
})
|
||||
|
||||
const varianceLoading = ref(false)
|
||||
|
||||
const currentItem = ref<StockItem | null>(null)
|
||||
@ -960,23 +1039,67 @@ const filteredVarianceList = computed(() => {
|
||||
)
|
||||
})
|
||||
|
||||
// ★ 新增: 获取盘点清单数据(后端分页)
|
||||
// ★ 新增: 获取盘点清单数据(合并全量应盘物资 + 已盘点记录)
|
||||
const fetchInventoryList = async () => {
|
||||
listLoading.value = true
|
||||
try {
|
||||
// 1. 获取已盘点记录
|
||||
const res: any = await request({
|
||||
url: '/v1/inbound/stock/draft/list',
|
||||
method: 'get',
|
||||
params: {
|
||||
page: listPage.value,
|
||||
limit: listLimit.value,
|
||||
page: 1,
|
||||
limit: 10000, // 获取全部已盘点记录
|
||||
keyword: listKeyword.value
|
||||
}
|
||||
})
|
||||
if (res) {
|
||||
listData.value = res.items || []
|
||||
listTotal.value = res.total || 0
|
||||
|
||||
const scannedDrafts = res?.items || []
|
||||
|
||||
// 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,
|
||||
sku: item.sku,
|
||||
material_name: item.material_name,
|
||||
spec_model: item.spec_model,
|
||||
stock_qty: item.stock_qty, // 账面数(盲盘时隐藏)
|
||||
quantity: draft?.quantity || 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)
|
||||
|
||||
} catch (e) {
|
||||
ElMessage.error('获取盘点清单失败')
|
||||
} finally {
|
||||
@ -989,6 +1112,10 @@ const openInventoryList = async () => {
|
||||
showList.value = true
|
||||
listPage.value = 1
|
||||
listKeyword.value = ''
|
||||
listStatusFilter.value = 'all'
|
||||
// 获取盘点基数(应盘物资清单)
|
||||
await fetchAllStockItems()
|
||||
// 获取已盘点列表
|
||||
await fetchInventoryList()
|
||||
}
|
||||
|
||||
@ -1010,6 +1137,30 @@ const handleListLimitChange = (limit: number) => {
|
||||
fetchInventoryList()
|
||||
}
|
||||
|
||||
// ★ 新增: 更新实盘数量(手动修改)
|
||||
const handleQuantityChange = async (row: any, val: number) => {
|
||||
try {
|
||||
await updateStocktakeQuantity({
|
||||
stock_id: row.stock_id,
|
||||
source_table: row.source_table,
|
||||
quantity: val
|
||||
})
|
||||
ElMessage.success('实盘数已更新')
|
||||
} catch (e) {
|
||||
console.error('更新实盘数失败', e)
|
||||
ElMessage.error('更新失败')
|
||||
// 重新获取列表数据
|
||||
fetchInventoryList()
|
||||
}
|
||||
}
|
||||
|
||||
// ★ 新增: 更新备注
|
||||
const handleRemarkChange = async (row: any) => {
|
||||
// 备注更新可以批量处理或直接调用后端接口
|
||||
// 这里暂时只更新本地数据,实际项目中可以调用后端保存
|
||||
console.log('备注更新:', row.remark, row.id)
|
||||
}
|
||||
|
||||
// ★ 修改:结束盘点按钮直接调用 finishStocktake,跳过二次确认弹窗
|
||||
const openFinishDialog = () => {
|
||||
if (stats.value.total === 0) {
|
||||
|
||||
Reference in New Issue
Block a user