@ -111,6 +111,32 @@
< / div >
< / div >
< / el-card >
< / el-card >
<!-- ★ 新增 : 防呆确认弹窗 -- >
< el-dialog
v-model = "showConfirmDialog"
title = "⚠️ 确认清除盘点数据"
width = "400"
:close-on-click-modal = "false"
:close-on-press-escape = "false"
show -close
align -center
>
< div class = "confirm-content" >
< el-icon :size = "48" color = "#E6A23C" > < WarningFilled / > < / el-icon >
< p class = "confirm-text" > 存在未完成记录 , 开始新盘点将清除它们 , 确定吗 ? < / p >
< / div >
< template # footer >
< el-button @click ="cancelConfirm" > 取消 < / el -button >
< el-button
type = "danger"
: disabled = "countdown > 0"
@click ="confirmClear"
>
{ { countdown > 0 ? ` 确认清除 ( ${ countdown } s) ` : '确认清除' } }
< / el-button >
< / template >
< / el-dialog >
< el-dialog
< el-dialog
v-model = "showQtyDialog"
v-model = "showQtyDialog"
title = "🔢 录入实盘数量"
title = "🔢 录入实盘数量"
@ -155,6 +181,19 @@
/ >
/ >
< p class = "unit-text" > 单位 : { { currentItem . unit || '个' } } < / p >
< p class = "unit-text" > 单位 : { { currentItem . unit || '个' } } < / p >
< / div >
< / div >
<!-- ★ 新增 : 备注输入框 -- >
< div class = "remark-area" style = "margin-top: 15px;" >
< el-form-item label = "备注:" label -width = " 60px " >
< el-input
v-model = "inputRemark"
placeholder = "选填,可填写差异原因说明"
type = "textarea"
:rows = "2"
clearable
/ >
< / el-form-item >
< / div >
< / div >
< / div >
< template # footer >
< template # footer >
< div class = "dialog-footer" >
< div class = "dialog-footer" >
@ -194,9 +233,9 @@
< el-table-column prop = "name" label = "名称" min -width = " 90 " show -overflow -tooltip / >
< el-table-column prop = "name" label = "名称" min -width = " 90 " show -overflow -tooltip / >
< el-table-column prop = "sku" label = "SKU" width = "110" show -overflow -tooltip / >
< el-table-column prop = "sku" label = "SKU" width = "110" show -overflow -tooltip / >
< el-table-column prop = "batch_no" label= "批次" width ="85 " show -overflow -tooltip >
< el-table-column label = "序列号/ 批次" min - width =" 150 " show -overflow -tooltip >
< template # default = "scope " >
< template # default = "{ row } " >
{ { scope . row . serial _number || scope . row . batch _no || '-' } }
< span > {{ row . serial _number || row . batch _number || row . batch _no || '-' } } < / span >
< / template >
< / template >
< / el-table-column >
< / el-table-column >
@ -266,12 +305,24 @@
style = "margin-bottom: 15px;"
style = "margin-bottom: 15px;"
>
>
< template # default >
< template # default >
以下列表显示所有已结束盘点但尚未平账的差异记录 。 请逐条 核实后, 点击 "确认平账" 按钮调整系统库存 。
以下列表显示所有已结束盘点但尚未平账的差异记录 。 请核实后进行后续处理 。
< / template >
< / template >
< / el-alert >
< / el-alert >
<!-- ★ 新增 : 差异列表搜索 -- >
< el-input
v-model = "searchSku"
placeholder = "输入 SKU/条码/名称快速定位"
clearable
style = "width: 250px; margin-bottom: 15px;"
>
< template # prefix >
< el-icon > < Search / > < / el-icon >
< / template >
< / el-input >
< el-table
< el-table
:data = "v arianceList"
:data = "filteredV arianceList"
height = "500"
height = "500"
border
border
stripe
stripe
@ -315,22 +366,9 @@
< el-tag v-else type = "warning" > 待审核 < / el-tag >
< el-tag v-else type = "warning" > 待审核 < / el-tag >
< / template >
< / template >
< / el-table-column >
< / el-table-column >
< el-table-column label = "操作" width = "120" align = "center" fixed = "right" >
< template # default = "scope" >
< el-button
v-if = "!scope.row.is_processed && userStore.hasPermission('inventory_stocktake:operation')"
type = "primary"
size = "small"
@click ="handleAdjust(scope.row)"
>
确认平账
< / el-button >
< span v-else class = "text-gray" > - < / span >
< / template >
< / el-table-column >
< / el-table >
< / el-table >
< el-empty v-if = "v arianceList.length === 0" description="暂无待审核的差异记录" / >
< el-empty v-if = "filteredV arianceList.length === 0" : description="searchSku ? '未找到匹配的差异记录' : ' 暂无待审核的差异记录' " / >
< / div >
< / div >
< template # footer >
< template # footer >
@ -353,7 +391,7 @@ import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
import { getAllStock } from '@/api/inbound/stock'
import { getAllStock } from '@/api/inbound/stock'
import QrScanner from '@/components/QrScanner/index.vue'
import QrScanner from '@/components/QrScanner/index.vue'
import { ElMessage , ElMessageBox } from 'element-plus'
import { ElMessage , ElMessageBox } from 'element-plus'
import { Search , VideoPlay , VideoPause , List , Checked , Download , ArrowRight , Cloudy , Edit , EditPen , CameraFilled , Close } from '@element-plus/icons-vue'
import { Search , VideoPlay , VideoPause , List , Checked , Download , ArrowRight , Cloudy , Edit , EditPen , CameraFilled , Close , WarningFilled } from '@element-plus/icons-vue'
import request from '@/utils/request'
import request from '@/utils/request'
import { useUserStore } from '@/stores/user'
import { useUserStore } from '@/stores/user'
@ -402,6 +440,16 @@ const showQtyDialog = ref(false)
// ★ 新增: 差异审核对话框
// ★ 新增: 差异审核对话框
const showVarianceDialog = ref ( false )
const showVarianceDialog = ref ( false )
// ★ 新增: 差异列表搜索
const searchSku = ref ( '' )
// ★ 新增: 盘点开始防呆倒计时
const countdown = ref ( 0 )
let countdownTimer : any = null
// ★ 新增: 防呆确认弹窗显示状态
const showConfirmDialog = ref ( false )
const allData = ref < StockItem [ ] > ( [ ] )
const allData = ref < StockItem [ ] > ( [ ] )
const scannedMap = ref < Map < string , number > > ( new Map ( ) )
const scannedMap = ref < Map < string , number > > ( new Map ( ) )
const borrowedQuantities = ref < Record < string , number > > ( { } )
const borrowedQuantities = ref < Record < string , number > > ( { } )
@ -416,6 +464,7 @@ const searchKeyword = ref('')
const currentItem = ref < StockItem | null > ( null )
const currentItem = ref < StockItem | null > ( null )
const inputQty = ref < number | undefined > ( undefined )
const inputQty = ref < number | undefined > ( undefined )
const inputRemark = ref ( '' )
const qtyInputRef = ref ( )
const qtyInputRef = ref ( )
// ★ 新增: 多人协同心跳刷新定时器
// ★ 新增: 多人协同心跳刷新定时器
@ -454,7 +503,14 @@ const syncData = async () => {
if ( res . materials ) res . materials . forEach ( ( i : any ) => { const item = processItem ( i , 'material' ) ; if ( item ) list . push ( item ) } )
if ( res . materials ) res . materials . forEach ( ( i : any ) => { const item = processItem ( i , 'material' ) ; if ( item ) list . push ( item ) } )
if ( res . semis ) res . semis . forEach ( ( i : any ) => { const item = processItem ( i , 'semi' ) ; if ( item ) list . push ( item ) } )
if ( res . semis ) res . semis . forEach ( ( i : any ) => { const item = processItem ( i , 'semi' ) ; if ( item ) list . push ( item ) } )
if ( res . products ) res . products . forEach ( ( i : any ) => { const item = processItem ( i , 'product' ) ; if ( item ) list . push ( item ) } )
if ( res . products ) res . products . forEach ( ( i : any ) => { const item = processItem ( i , 'product' ) ; if ( item ) list . push ( item ) } )
// ★ 强制按 SKU 数字+字符串升序排序
list . sort ( ( a , b ) => {
const skuA = a . sku || '' ;
const skuB = b . sku || '' ;
return skuA . localeCompare ( skuB , undefined , { numeric : true , sensitivity : 'base' } ) ;
} ) ;
// 静默更新数据( 不触发loading)
// 静默更新数据( 不触发loading)
allData . value = list
allData . value = list
await fetchBorrowedQuantities ( list )
await fetchBorrowedQuantities ( list )
@ -492,19 +548,6 @@ const api = {
method : 'get' ,
method : 'get' ,
params : { }
params : { }
} ) ,
} ) ,
// ★ 新增: 单条库存调整
adjustStock : ( draftId : number , stockId : number , diffQty : number , sourceTable : string , remark : string ) => request ( {
url : '/v1/inbound/stock/adjust' ,
method : 'post' ,
data : {
draft _id : draftId ,
stock _id : stockId , // 库存项ID
diff _qty : diffQty , // 差异数量(支持无草稿模式)
source _table : sourceTable , // 必须: stock_buy / stock_semi / stock_product
operator _name : currentUser ,
remark : remark
}
} ) ,
// ★ 保留清除功能(用于兼容性)
// ★ 保留清除功能(用于兼容性)
clearDraft : ( ) => request ( {
clearDraft : ( ) => request ( {
url : '/v1/inbound/stock/draft/clear' ,
url : '/v1/inbound/stock/draft/clear' ,
@ -571,11 +614,27 @@ const checkServerDraft = async () => {
// ★ 重写: 开始新盘点 - 使用新 API
// ★ 重写: 开始新盘点 - 使用新 API
const startNewSession = async ( ) => {
const startNewSession = async ( ) => {
// ★ 新增: 防呆确认弹窗
if ( serverDraftCount . value > 0 ) {
showConfirmDialog . value = true
countdown . value = 10
if ( countdownTimer ) clearInterval ( countdownTimer )
countdownTimer = setInterval ( ( ) => {
countdown . value --
if ( countdown . value <= 0 ) {
clearInterval ( countdownTimer ! )
countdownTimer = null
}
} , 1000 )
return
}
await doStartNewSession ( )
}
const doStartNewSession = async ( ) => {
showConfirmDialog . value = false
btnLoading . value = true
try {
try {
if ( serverDraftCount . value > 0 ) {
await ElMessageBox . confirm ( '存在未完成记录,开始新盘点将清除它们,确定吗?' , '警告' , { type : 'warning' } )
}
btnLoading . value = true
// 调用新 API 开始新会话
// 调用新 API 开始新会话
const res : any = await api . startNewSession ( )
const res : any = await api . startNewSession ( )
currentSessionId . value = res . session _id || ''
currentSessionId . value = res . session _id || ''
@ -592,6 +651,20 @@ const startNewSession = async () => {
} finally { btnLoading . value = false }
} finally { btnLoading . value = false }
}
}
const confirmClear = ( ) => {
if ( countdown . value > 0 ) return
doStartNewSession ( )
}
const cancelConfirm = ( ) => {
showConfirmDialog . value = false
if ( countdownTimer ) {
clearInterval ( countdownTimer )
countdownTimer = null
}
countdown . value = 0
}
// ★ 重写: 继续上次盘点 - 恢复扫码作业
// ★ 重写: 继续上次盘点 - 恢复扫码作业
const resumeSession = async ( ) => {
const resumeSession = async ( ) => {
btnLoading . value = true
btnLoading . value = true
@ -693,6 +766,13 @@ const loadData = async () => {
if ( res . semis ) res . semis . forEach ( ( i : any ) => processItem ( i , 'semi' ) )
if ( res . semis ) res . semis . forEach ( ( i : any ) => processItem ( i , 'semi' ) )
if ( res . products ) res . products . forEach ( ( i : any ) => processItem ( i , 'product' ) )
if ( res . products ) res . products . forEach ( ( i : any ) => processItem ( i , 'product' ) )
// ★ 强制按 SKU 数字+字符串升序排序
list . sort ( ( a , b ) => {
const skuA = a . sku || '' ;
const skuB = b . sku || '' ;
return skuA . localeCompare ( skuB , undefined , { numeric : true , sensitivity : 'base' } ) ;
} ) ;
allData . value = list
allData . value = list
await fetchBorrowedQuantities ( list )
await fetchBorrowedQuantities ( list )
} catch ( e ) {
} catch ( e ) {
@ -760,7 +840,6 @@ const handleManualInput = async () => {
ElMessage . error ( ` 不在库条码: ${ code } ` )
ElMessage . error ( ` 不在库条码: ${ code } ` )
}
}
} finally {
} finally {
barcodeInput . value = ''
loading . value = false
loading . value = false
}
}
}
}
@ -780,8 +859,10 @@ const handleManualConfirm = () => {
if ( ! currentItem . value ) return
if ( ! currentItem . value ) return
const val = inputQty . value === undefined ? 0 : inputQty . value
const val = inputQty . value === undefined ? 0 : inputQty . value
updateAndSync ( currentItem . value , val )
updateAndSync ( currentItem . value , val , inputRemark . value )
showQtyDialog . value = false
showQtyDialog . value = false
// 重置备注
inputRemark . value = ''
ElMessage . success ( ` 已记录实盘: ${ val } ` )
ElMessage . success ( ` 已记录实盘: ${ val } ` )
// ★★★ 核心修改:确认数量后自动重新打开全屏扫码,实现无缝闭环
// ★★★ 核心修改:确认数量后自动重新打开全屏扫码,实现无缝闭环
@ -792,14 +873,14 @@ const handleManualConfirm = () => {
} )
} )
}
}
const updateAndSync = async ( item : StockItem , quantity : number ) => {
const updateAndSync = async ( item : StockItem , quantity : number , remark : string = '' ) => {
item . scanned = true
item . scanned = true
item . qty _actual = quantity
item . qty _actual = quantity
scannedMap . value . set ( item . uuid , quantity )
scannedMap . value . set ( item . uuid , quantity )
syncStatus . value = 'syncing'
syncStatus . value = 'syncing'
try {
try {
await api . addDraft ( { uuid : item . uuid , quantity : quantity } )
await api . addDraft ( { uuid : item . uuid , quantity : quantity , remark : remark } )
syncStatus . value = 'success'
syncStatus . value = 'success'
} catch ( e ) {
} catch ( e ) {
syncStatus . value = 'failed'
syncStatus . value = 'failed'
@ -888,6 +969,18 @@ const varianceList = computed(() => {
} ) )
} ) )
} )
} )
// ★ 新增: 本地搜索过滤后的差异列表
const filteredVarianceList = computed ( ( ) => {
if ( ! searchSku . value ) return varianceList . value
const kw = searchSku . value . toLowerCase ( )
return varianceList . value . filter ( i =>
( i . uuid && i . uuid . toLowerCase ( ) . includes ( kw ) ) ||
( i . sku && i . sku . toLowerCase ( ) . includes ( kw ) ) ||
( i . stock _name && i . stock _name . toLowerCase ( ) . includes ( kw ) ) ||
( i . stock _spec && i . stock _spec . toLowerCase ( ) . includes ( kw ) )
)
} )
const openInventoryList = ( ) => { showList . value = true }
const openInventoryList = ( ) => { showList . value = true }
// ★ 修改:结束盘点按钮直接调用 finishStocktake, 跳过二次确认弹窗
// ★ 修改:结束盘点按钮直接调用 finishStocktake, 跳过二次确认弹窗
@ -946,35 +1039,6 @@ const openVarianceDialog = async () => {
varianceLoading . value = false
varianceLoading . value = false
}
}
// ★ 新增: 确认平账
const handleAdjust = async ( row : any ) => {
try {
// ===== 调试代码 =====
console . warn ( '---- 准备平账参数检查 ----' ) ;
console . warn ( '当前点击行的完整数据:' , row ) ;
console . warn ( ` 将要发送的 draftId: ${ row . id } , stockId: ${ row . stock _id } , sourceTable: ${ row . source _table } ` ) ;
// ===== 调试结束 =====
await ElMessageBox . confirm (
` 确定要对 " ${ row . uuid } " 进行平账调整吗? \ n \ n差异: ${ row . diff _qty > 0 ? '盘盈 +' : '盘亏 ' } ${ row . diff _qty } ` ,
'确认平账' ,
{ type : 'warning' , confirmButtonText : '确认调整' , cancelButtonText : '取消' }
)
const remark = ` 盘点差异调整 - ${ row . diff _qty > 0 ? '盘盈入库' : '盘亏出库' } `
const res : any = await api . adjustStock ( row . id , row . stock _id , row . diff _qty , row . source _table || 'stock_buy' , remark )
ElMessage . success ( res . message || '调整成功' )
// 刷新数据并重新打开差异列表
await loadData ( )
await openVarianceDialog ( )
} catch ( e : any ) {
if ( e !== 'cancel' ) ElMessage . error ( e ? . message || '操作失败' )
}
}
// ★ 新增: 跳转到差异审核页面
// ★ 新增: 跳转到差异审核页面
const goToVarianceReview = ( ) => {
const goToVarianceReview = ( ) => {
openVarianceDialog ( )
openVarianceDialog ( )
@ -1239,4 +1303,14 @@ const goToVarianceReview = () => {
width : 100 % ;
width : 100 % ;
}
}
}
}
. confirm - content {
text - align : center ;
padding : 20 px 0 ;
}
. confirm - content . confirm - text {
margin - top : 20 px ;
font - size : 16 px ;
color : # 303133 ;
}
< / style >
< / style >