@ -4,62 +4,163 @@
< template # header >
< div class = "card-header" >
< div class = "title-box" >
< span > 报废作业 < / span >
< span > 按单报废执行 < / span >
< el-tag v-if = "cartItems.length > 0" type="danger" size="small" effect="dark" >
已选 {{ cartItems.length }} 项
已扫 {{ cartItems.length }} 项
< / el -tag >
< / div >
< / div >
< / template >
< div class = "scan-section " >
< div v-if = "hasPermission" class="camera-placeholder" @click="showCamera = true" >
< el -icon :size = "40" color = "#409EFF" > < CameraFilled / > < / el-ico n>
< span class = "text" > 点击开启全屏扫码 < / span >
< div class = "mode-switch-bar " >
< el-tag type = "danger" size = "large" style = "font-size: 14px;" > 按单执行 < / el-tag >
< span class = "mode-hint" > 请先选择已审批通过的报废申请单 , 再扫码报废 < / spa n>
< / div >
<!-- Step 1 : 选择已批准的报废申请单 -- >
< div class = "approval-request-select" >
< el-select
v-model = "selectedRequestId"
placeholder = "请选择已审批通过的报废申请单"
filterable
clearable
style = "width: 100%"
:loading = "requestsLoading"
@change ="handleRequestChange"
>
< el-option
v-for = "req in approvalRequests"
:key = "req.id"
:value = "req.id"
:label = "req.request_no"
>
< span > { { req . request _no } } < / span >
< el-divider direction = "vertical" / >
< span > { { req . applicant _name || '未知申请人' } } < / span >
< el-divider direction = "vertical" / >
< span style = "color: #909399; font-size: 13px" > { { req . remark || '无备注' } } < / span >
< / el-option >
< / el-select >
< p class = "select-tip" > 仅显示已通过 ( status = 1 ) 的报废申请单 < / p >
< / div >
<!-- Step 2 : 待执行清单 -- >
< div v-if = "selectedRequest" class="planned-items-section" >
< div class = "planned-header" >
< span class = "planned-title" > 待执行报废清单 < / span >
< el-tag type = "danger" size = "small" > { { plannedItems . length } } 种 < / el-tag >
< / div >
< div v-else class = "camera-placehol der" styl e = "background-color: #f5f5f5; cursor: not-allowed ;" >
< el-icon :size = "40" color = "#909399" > < CameraFilled / > < / el-icon >
< spa n c lass = "text" > 无扫码权限 < / span >
< el-table :data = "plannedItems" bor der siz e = "small" style = "width: 100% ;" >
< el-table-column type = "index" label = "序号" width = "60" align = "center" / >
< el-table-colum n label = "来源" width = "80" align = "center" >
< template # default = "{ row }" >
< el-tag size = "small" type = "info" > { { sourceLabel ( row . source _table ) } } < / el-tag >
< / template >
< / el-table-column >
< el-table-column prop = "name" label = "名称" min -width = " 120 " show -overflow -tooltip / >
< el-table-column prop = "spec_model" label = "规格" min -width = " 100 " show -overflow -tooltip / >
< el-table-column prop = "sku" label = "SKU" width = "110" show -overflow -tooltip / >
< el-table-column prop = "location" label = "库位" width = "100" show -overflow -tooltip / >
< el-table-column label = "批准数量" width = "90" align = "center" >
< template # default = "{ row }" >
< span style = "color: #E6A23C; font-weight: bold;" > { { row . scrap _qty ? ? '-' } } < / span >
< / template >
< / el-table-column >
< el-table-column label = "已扫" width = "80" align = "center" >
< template # default = "{ row }" >
< span : style = "{ color: scannedQtyOf(row) >= Number(row.scrap_qty) ? '#67C23A' : '#909399', fontWeight: 'bold' }" >
{ { scannedQtyOf ( row ) } }
< / span >
< / template >
< / el-table-column >
< / el-table >
< / div >
< div class = "scan-section" >
<!-- 扫码进度条 -- >
< div v-if = "selectedRequest && scanTotalQty > 0" class="scan-progress" >
< div class = "progress-info" >
< span > 扫码进度 < / span >
< el-tag type = "success" size = "small" > { { scanScannedTypes } } / { { scanTotalTypes } } 种 < / el-tag >
< el-tag type = "primary" size = "small" > { { scanScannedQty } } / { { scanTotalQty } } 件 < / el-tag >
< el-button
v-if = "unscannedCount > 0"
type = "warning" plain size = "small" style = "margin-left: auto;"
@click ="showUnscannedDialog = true"
>
未扫清单 ( { { unscannedCount } } )
< / el-button >
< / div >
< el-progress
: percentage = "scanTotalQty > 0 ? Math.min(100, Math.round(scanScannedQty / scanTotalQty * 100)) : 0"
:stroke-width = "8"
: color = "scanScannedQty >= scanTotalQty ? '#67C23A' : '#F56C6C'"
/ >
< / div >
< div class = "input-box" >
< el-input
v-model = "barcodeInput"
p laceholder = "扫描或输入条码回车"
@keyup.enter ="handleManualInput"
clearable
ref = "barcodeRef"
size = "large"
:disabled = "!hasPermission"
>
< template # prefix >
< el-icon > < Scissor / > < / el-icon >
< / templat e >
< template # append >
< el-button @click ="handleManualInput" :disabled = "!hasPermission" > 添加 < / el-butt on >
< / template >
< / el-input >
< / div >
< template v-if = "!selectedRequestId" >
< div class = "camera-placeholder" style = "background-color: #f5f5f5; cursor: not-allowed;" >
< el-icon :size = "40" color = "#909399" > < CameraFilled / > < / el-icon >
< span c lass = "text" > 请先选择报废申请单 < / span >
< / div >
< div class = "input-box" >
< el-input v-model = "barcodeInput" placeholder="请先选择报废申请单" disabled size="large" >
< template # prefix > < el-icon > < Scissor / > < / el-icon > < / template >
< template # append > < el-button disabled > 添加 < / el-button > < / template >
< / el-input >
< / div >
< / template >
< template v-els e>
< div v-if = "hasPermission" class="camera-placeholder" @click="showCamera = true" >
< el-icon :size = "40" color = "#409EFF" > < CameraFilled / > < / el-ic on >
< span class = "text" > 点击开启全屏扫码 < / span >
< / div >
< div v-else class = "camera-placeholder" style = "background-color: #f5f5f5; cursor: not-allowed;" >
< el-icon :size = "40" color = "#909399" > < CameraFilled / > < / el-icon >
< span class = "text" > 无扫码权限 < / span >
< / div >
< div class = "input-box" >
< el-input
v-model = "barcodeInput"
placeholder = "扫描或输入条码回车"
@keyup.enter ="handleManualInput"
clearable
ref = "barcodeRef"
size = "large"
:disabled = "!hasPermission"
>
< template # prefix >
< el-icon > < Scissor / > < / el-icon >
< / template >
< template # append >
< el-button @click ="handleManualInput" :disabled = "!hasPermission" > 添加 < / el-button >
< / template >
< / el-input >
< / div >
< / template >
< / div >
< div class = "cart-section" >
< div v-if = "cartItems.length > 0" >
< el -table :data = "cartItems" border stripe style = "width: 100%" >
< el-table-column prop = "name" label = "物品名称" min -width = " 120 " show -overflow -tooltip / >
< el-table-column prop = "spec_model" label = "规格" min -width = " 100 " show -overflow -tooltip / >
< el-table-column prop = "sku" label = "SKU" width = "120" show -overflow -tooltip / >
< el-table-column label = "可用 库存" width = "9 0" align = "center" >
< el-table-column label = "库存" width = "7 0" align = "center" >
< template # default = "{row}" >
< el-tag type = "info" > { { parseFloat ( row . available _quantity) } } < / el-tag >
< el-tag : type = "parseFloat(row. available_quantity) > 0 ? 'success' : 'danger'" size = "small" >
{ { parseFloat ( row . available _quantity ) } }
< / el-tag >
< / template >
< / el-table-column >
< el-table-column label = "报废数" width = "130" align = "center" >
< template # default = "{row}" >
< el-input-number
v-model = "row.quantity"
v-model = "row.scrap_ quantity"
:min = "1"
:max = "parseFloat(row.available_quantity )"
:max = "maxScrapFor(row )"
size = "small"
style = "width: 100px"
:disabled = "!hasPermission"
@ -67,14 +168,6 @@
< / template >
< / el-table-column >
< el-table-column label = "损失金额" width = "100" align = "center" >
< template # default = "{row}" >
< span style = "color: #F56C6C; font-weight: bold;" >
¥ { { ( ( row . price || 0 ) * row . quantity ) . toFixed ( 2 ) } }
< / span >
< / template >
< / el-table-column >
< el-table-column v-if = "hasPermission" label="操作" width="60" align="center" fixed="right" >
< template # default = "{$index}" >
< el-button type = "danger" icon = "Delete" circle size = "small" @click ="removeFromCart($index)" / >
@ -86,29 +179,17 @@
< / div >
< div v-if = "cartItems.length > 0" class="form-section" >
< el -divider content -position = " left " > 报废登记 信息 < / el-divider >
< el -divider content -position = " left " > 报废执行 信息 < / el-divider >
< el-form :model = "form" ref = "formRef" label -position = " top " >
< el-form-item label = "报废原因" prop = "reason" required >
< el-input
v-model = "form.reason"
type = "textarea"
:rows = "3"
placeholder = "请详细填写报废原因,如:设备老化、损坏严重等"
size = "large"
maxlength = "500"
show -word -limit
/ >
< / el-form-item >
< el-form-item label = "备注说明" prop = "remark" >
< el-input v-model = "form.remark" type="textarea" :rows="2" placeholder="补充说明..." / >
< el-form :model = "form" label -position = " top " >
< el-form-item label = "报废原因(来自申请单) " prop = "reason" >
< el-input v-model = "form.reason" type="textarea" :rows="2" disabled / >
< / el-form-item >
< div class = "bottom-actions" >
< el-button v-if = "hasPermission" @click="clearAll" icon="Refresh" > 清空 < / el -button >
< el-button v-if = "hasPermission" @click="clearAll" icon="Refresh" > 重扫 < / el -button >
< el-button v-if = "hasPermission" type="danger" size="large" :loading="loading" @click="submitForm" icon="Select" >
确认报废
确认执行 报废
< / el -button >
< / div >
< / el-form >
@ -127,26 +208,60 @@
< / div >
< div class = "scanner-footer" >
< p > 请将条码 / 二维码放入镜头范围 < / p >
< p v-if = "cartItems.length > 0" class="current-count" > 已添加 : {{ cartItems.length }} 项 < / p >
< p > 请对准条形码 , 识别成功后自动添加 < / p >
< p v-if = "cartItems.length > 0" class="current-count" > 当前 已添加: {{ s canScannedQty }} 件 < / p >
< / div >
< / div >
< ! - - 未扫清单弹窗 - - >
< el-dialog v-model = "showUnscannedDialog" title="未扫清单" width="600px" destroy -on -close >
< el-alert
v-if = "unscannedList.length > 0"
: title = "`以下 ${unscannedList.length} 种物料还未扫满,请补扫(未扫满也可提交,将按实扫数量报废):`"
type = "warning" :closable = "false" show -icon style = "margin-bottom: 12px;"
/ >
< el-table :data = "unscannedList" border size = "small" max -height = " 400 " >
< el-table-column type = "index" label = "#" width = "40" align = "center" / >
< el-table-column prop = "name" label = "物料名称" min -width = " 140 " show -overflow -tooltip / >
< el-table-column prop = "sku" label = "SKU" min -width = " 110 " show -overflow -tooltip / >
< el-table-column label = "批准" width = "70" align = "center" >
< template # default = "{ row }" > { { row . planQty } } < / template >
< / el-table-column >
< el-table-column label = "已扫" width = "70" align = "center" >
< template # default = "{ row }" >
< span style = "color: #67C23A;" > { { row . scannedQty } } < / span >
< / template >
< / el-table-column >
< el-table-column label = "待扫" width = "70" align = "center" >
< template # default = "{ row }" >
< span style = "color: #F56C6C; font-weight: bold;" > { { row . remaining } } < / span >
< / template >
< / el-table-column >
< / el-table >
< template # footer >
< el-button @click ="showUnscannedDialog = false" > 关闭 < / el -button >
< el-button type = "primary" @click ="showUnscannedDialog = false; showCamera = true" > 去扫码 < / el -button >
< / template >
< / el-dialog >
< / div >
< / template >
< script setup lang = "ts" >
import { ref , reactive , nextTick } from 'vue'
import { ref , reactive , nextTick , computed , onMounted } from 'vue'
import { ElMessage , ElMessageBox } from 'element-plus'
import { Scissor , CameraFilled , Close , Refresh , Select , Delete } from '@element-plus/icons-vue'
import { Scissor , CameraFilled } from '@element-plus/icons-vue'
import QrScanner from '@/components/QrScanner/index.vue'
import { scanBarcode , createScrap } from '@/api/scrap'
import { scanBarcode , getScrapApprovals , executeScrapByRequest } from '@/api/scrap'
import { useUserStore } from '@/stores/user'
import { useRouter } from 'vue-router'
import { useRouter , useRoute } from 'vue-router'
const router = useRouter ( )
const route = useRoute ( )
const userStore = useUserStore ( )
const hasPermission = userStore . hasPermission ( 'scrap_create:operation' )
const hasPermission = computed ( ( ) =>
userStore . role === 'SUPER_ADMIN' || userStore . hasPermission ( 'scrap_execute' )
)
// --- 状态定义 ---
const barcodeInput = ref ( '' )
@ -154,14 +269,144 @@ const cartItems = ref<any[]>([])
const loading = ref ( false )
const showCamera = ref ( false )
const barcodeRef = ref ( )
const formRef = ref ( )
// 已批准的报废申请单
const approvalRequests = ref < any [ ] > ( [ ] )
const selectedRequest = ref < any > ( null )
const requestsLoading = ref ( false )
const showUnscannedDialog = ref ( false )
const form = reactive ( {
reason : '' ,
remark : ''
reason : ''
} )
// --- 核心扫码逻辑 ---
// --- 计算属性 ---
const selectedRequestId = computed ( {
get : ( ) => selectedRequest . value ? . id ? ? null ,
set : ( val ) => {
if ( ! val ) {
selectedRequest . value = null
} else {
selectedRequest . value = approvalRequests . value . find ( r => r . id === val ) ? ? null
}
}
} )
const plannedItems = computed < any [ ] > ( ( ) => selectedRequest . value ? . items ? ? [ ] )
// 扫码进度
const scanTotalQty = computed ( ( ) =>
plannedItems . value . reduce ( ( sum : number , it : any ) => sum + ( Number ( it . scrap _qty ) || 0 ) , 0 )
)
const scanScannedQty = computed ( ( ) =>
cartItems . value . reduce ( ( sum : number , it : any ) => sum + ( Number ( it . scrap _quantity ) || 0 ) , 0 )
)
const scanScannedTypes = computed ( ( ) => cartItems . value . length )
const scanTotalTypes = computed ( ( ) => plannedItems . value . length )
// --- 按 (source_table, stock_id) 匹配计划项 ---
const matchPlan = ( sourceTable : string , stockId : any ) =>
plannedItems . value . find (
( p : any ) => String ( p . source _table ) === String ( sourceTable ) && String ( p . stock _id ) === String ( stockId )
)
// 某计划项已扫数量
const scannedQtyOf = ( plan : any ) =>
cartItems . value
. filter ( ci => String ( ci . source _table ) === String ( plan . source _table )
&& String ( ci . stock _id ) === String ( plan . stock _id ) )
. reduce ( ( sum , ci ) => sum + ( Number ( ci . scrap _quantity ) || 0 ) , 0 )
// 购物车某行还能再报废多少(批准量 - 其余行已占用量)
const maxScrapFor = ( row : any ) => {
const plan = matchPlan ( row . source _table , row . stock _id )
const approved = Number ( plan ? . scrap _qty ) || 0
const others = cartItems . value
. filter ( ci => ci !== row
&& String ( ci . source _table ) === String ( row . source _table )
&& String ( ci . stock _id ) === String ( row . stock _id ) )
. reduce ( ( sum , ci ) => sum + ( Number ( ci . scrap _quantity ) || 0 ) , 0 )
const avail = parseFloat ( row . available _quantity ) || 0
return Math . max ( 0 , Math . min ( approved - others , avail ) )
}
// 未扫清单
const unscannedList = computed ( ( ) =>
plannedItems . value . map ( ( plan : any ) => {
const planQty = Number ( plan . scrap _qty ) || 0
const scannedQty = scannedQtyOf ( plan )
return {
name : plan . name || '' ,
sku : plan . sku || '' ,
planQty ,
scannedQty ,
remaining : Math . max ( 0 , planQty - scannedQty )
}
} ) . filter ( ( item : any ) => item . remaining > 0 )
)
const unscannedCount = computed ( ( ) => unscannedList . value . length )
const sourceLabel = ( st : string ) =>
( { stock _buy : '采购件' , stock _semi : '半成品' , stock _product : '成品' } as any ) [ st ] || st || '-'
// --- 加载已批准的报废申请单 ---
const loadApprovalRequests = async ( ) => {
requestsLoading . value = true
try {
const res : any = await getScrapApprovals ( { scope : 'executable' , status : 1 , page : 1 , limit : 100 } )
approvalRequests . value = res . data ? . items || [ ]
// 支持从审批页跳转时带 requestId 直接选中
const presetId = Number ( route . query . requestId )
if ( presetId ) {
const hit = approvalRequests . value . find ( r => r . id === presetId )
if ( hit ) handleRequestChange ( presetId )
else ElMessage . warning ( '指定的报废申请单不可执行(可能已被执行或状态已变更)' )
}
} catch ( e ) {
console . error ( '加载报废申请单失败' , e )
ElMessage . error ( '加载报废申请单失败' )
} finally {
requestsLoading . value = false
}
}
const handleRequestChange = ( val : number | null ) => {
if ( ! val ) {
selectedRequest . value = null
form . reason = ''
} else {
selectedRequest . value = approvalRequests . value . find ( r => r . id === val ) ? ? null
form . reason = selectedRequest . value ? . remark || ''
}
// 切换申请单时清空购物车,防止已扫物品与新单据混淆
cartItems . value = [ ]
barcodeInput . value = ''
}
/**
* 校验扫码物品是否属于本申请单的批准明细,且累计不超批准量。
* 返回 null 表示通过,否则返回错误文案。
*/
const validateAgainstPlan = ( item : any , addQty : number ) : string | null => {
const plan = matchPlan ( item . source _table , item . id )
if ( ! plan ) {
return ` 该物料【 ${ item . name || item . sku } 】不在申请单的批准明细中,禁止报废 `
}
const approved = Number ( plan . scrap _qty ) || 0
const alreadyScanned = scannedQtyOf ( plan )
if ( alreadyScanned + addQty > approved ) {
return ` 【 ${ plan . name || plan . sku } 】超出批准数量(批准: ${ approved } ,已扫: ${ alreadyScanned } ,本次: ${ addQty } ) `
}
return null
}
// --- 扫码逻辑 ---
const onScanSuccess = ( code : string ) => {
if ( ! code ) return
const trimCode = code . trim ( )
@ -184,48 +429,95 @@ const onScanSuccess = (code: string) => {
}
const handleManualInput = async ( ) => {
if ( ! hasPermission . value ) {
ElMessage . warning ( '无操作权限' )
return
}
const code = barcodeInput . value . trim ( )
if ( ! code ) return
if ( ! selectedRequest . value ) {
ElMessage . warning ( '请先选择要执行的报废申请单' )
return
}
try {
loading . value = true
// 查重
const existIndex = cartItems . value . findIndex ( item => item . barcode === code || item . sku === code )
// 1. 购物车已有该库存行 → 累加(按 source_table + id 去重,与申请单明细同口径)
const existIndex = cartItems . value . findIndex (
item => String ( item . source _table ) === String ( item . source _table ) &&
( item . barcode === code || item . sku === code || String ( item . id ) === String ( code ) )
)
if ( existIndex > - 1 ) {
const item = cartItems . value [ existIndex ]
const maxQty = parseFloat ( item . available _quantity )
if ( item . quantity < maxQty ) {
item . quantity ++
ElMessage . success ( ` 数量+1 (当前: ${ item . quantity } ) ` )
if ( navigator . vibrate ) navigat or. vibrate ( 50 )
} else {
ElMessage . warning ( ` 库存不足 (余: ${ maxQty } ) ` )
// ★ 越权/超量拦截:红色错误提示并阻断扫码
const err = validateAgainstPlan ( item , 1 )
if ( err ) {
ElMessage . err or( err )
if ( navigator . vibrate ) navigator . vibrate ( [ 200 , 100 , 200 ] )
barcodeInput . value = ''
return
}
const maxQty = maxScrapFor ( item )
if ( maxQty <= 0 ) {
ElMessage . error ( ` 【 ${ item . name } 】已达批准数量上限,无法继续报废 ` )
if ( navigator . vibrate ) navigator . vibrate ( [ 200 , 100 , 200 ] )
barcodeInput . value = ''
return
}
try {
await ElMessageBox . confirm (
` 【 ${ item . name } × ${ item . spec _model || '-' } 】已在清单中(当前已扫 ${ item . scrap _quantity } 个)。 \ n \ n确认再 +1 吗? ` ,
'重复扫码确认' ,
{ confirmButtonText : '确认 +1' , cancelButtonText : '取消' , type : 'warning' }
)
} catch ( e ) {
barcodeInput . value = ''
return
}
item . scrap _quantity ++
ElMessage . success ( ` 数量+1 (当前: ${ item . scrap _quantity } ) ` )
if ( navigator . vibrate ) navigator . vibrate ( 50 )
barcodeInput . value = ''
return
}
// 查库
const res = await scanBarcode ( code )
// 2. 查库
const res : any = await scanBarcode ( code )
if ( res . code === 200 && res . data ) {
const item = res . data
const availQty = parseFloat ( item . available _quantity || 0 )
if ( availQty <= 0 ) {
ElMessage . warning ( ` 库存不足 (余: ${ availQty } ) ` )
} else {
cartItems . value . push ( {
... item ,
quantity : 1 ,
price : item . price || 0
} )
ElMessage . success ( ` 添加成功: ${ item . name } ` )
if ( navigator . vibrate ) navigator . vibrate ( 100 )
// ★ 校验是否在批准明细内 / 是否超批准量
const err = validateAgainstPlan ( item , 1 )
if ( err ) {
ElMessage . error ( err )
if ( navigator . vibrate ) navigator . vibrate ( [ 200 , 100 , 200 ] )
barcodeInput . value = ''
return
}
const availQty = parseFloat ( item . available _quantity || 0 )
if ( availQty <= 0 ) {
ElMessage . error ( ` 库存不足 (余: ${ availQty } ) ` )
if ( navigator . vibrate ) navigator . vibrate ( [ 200 , 100 , 200 ] )
barcodeInput . value = ''
return
}
cartItems . value . push ( {
... item ,
scrap _quantity : 1
} )
ElMessage . success ( ` 添加成功: ${ item . name } ` )
if ( navigator . vibrate ) navigator . vibrate ( 100 )
barcodeInput . value = ''
} else {
ElMessage . error ( res . msg || '未找到该物料' )
if ( navigator . vibrate ) navigator . vibrate ( [ 200 , 100 , 200 ] )
}
} catch ( error : any ) {
if ( error . response && error . response . status === 404 ) {
@ -233,6 +525,7 @@ const handleManualInput = async () => {
} else {
ElMessage . error ( '查询出错' )
}
if ( navigator . vibrate ) navigator . vibrate ( [ 200 , 100 , 200 ] )
} finally {
loading . value = false
if ( ! showCamera . value ) {
@ -242,251 +535,190 @@ const handleManualInput = async () => {
}
const removeFromCart = ( index : number ) => {
if ( ! hasPermission . value ) {
ElMessage . warning ( '无操作权限' )
return
}
cartItems . value . splice ( index , 1 )
}
const clearAll = ( ) => {
ElMessageBox . confirm ( '确定清空所有已选物品吗?' , '提示' , { type : 'warning' } )
if ( ! hasPermission . value ) {
ElMessage . warning ( '无操作权限' )
return
}
ElMessageBox . confirm ( '确定清空所有已扫物品吗?(保留申请单选择)' , '提示' , { type : 'warning' } )
. then ( ( ) => {
cartItems . value = [ ]
form . reason = ''
form . remark = ''
barcodeInput . value = ''
} )
. catch ( ( ) => { } )
}
// --- 提交逻辑 ---
// --- 提交执行 ---
const submitForm = async ( ) => {
if ( ! formRef . value ) return
if ( cartItems . value . length === 0 ) return ElMessage . warning ( '请先添加物品 ' )
// 验证报废原因
if ( ! form . reason . trim ( ) ) {
ElMessage . error ( '请填写报废原因' )
if ( ! hasPermission . value ) {
ElMessage . warning ( '无操作权限 ' )
return
}
if ( ! selectedRequest . value ) return ElMessage . warning ( '请先选择要执行的报废申请单' )
if ( cartItems . value . length === 0 ) return ElMessage . warning ( '请先扫码添加报废物料' )
// 检查报废数 量
const invalidI tem = cartItems . value . find ( item => ! item . quantity || item . quantity <= 0 )
if ( invalidI tem) {
ElMessage . warning ( '请填写有效的报废数量' )
return
// 逐项复核批准 量
for ( const item of cartItems . value ) {
const err = validateAgainstPlan ( i tem , 0 )
if ( err ) return ElMessage . error ( err )
if ( ! item . scrap _quantity || item . scrap _quantity <= 0 ) {
return ElMessage . warning ( '请填写有效的报废数量' )
}
if ( item . scrap _quantity > ( parseFloat ( item . available _quantity ) || 0 ) ) {
return ElMessage . error ( ` 【 ${ item . name } 】报废数量超过当前可用库存 ` )
}
}
// 检查报废数量是否超过可用库存
const overstockItem = cartItems . value . find ( item => item . quantity > parseFloat ( item . available _quantity ) )
if ( overstock Item) {
ElMessage . warning ( ` 物料 ${ overstockItem . sku } 报废数量超过可用库存 ` )
return
}
try {
await ElMessageBox . confirm (
` 确认按实扫数量执行报废吗? \ n申请单: ${ selectedRequest . value . request _no } \ n共 ${ cart Items . value . length } 项 / ${ scanScannedQty . value } 件 \ n \ n执行后不可撤销。 ` ,
'执行确认' ,
{ confirmButtonText : '确认执行' , cancelButtonText : ' 取消 ', type: ' warning ' }
)
} catch { return }
try {
loading.value = true
const data = {
reason : form . reason ,
remark : form . remark ,
items : cartItems . value . map ( item => ( {
id : item . id ,
sku : item . sku ,
source _table : item . source _table ,
quantity : item . quantity
} ) )
}
const items = cartItems.value.map(item => ({
source_table: item.source_table,
stock_id: item.id,
quantity: item.scrap_quantity,
sku: item.sku ? String(item.sku) : ' ',
name: item.name ? String(item.name) : ' '
}))
const res = await createScrap ( data )
if ( res . code === 200 ) {
ElMessage. success( '报废提交成功' )
router. push ( '/scrap/index' )
} else {
ElMessage . error ( res . msg || '提交失败' )
}
} catch ( error : any ) {
console . error ( error )
ElMessage . error ( error . response ? . data ? . msg || '提交失败' )
await executeScrapByRequest(selectedRequest.value.id, items)
ElMessage. success(' 已执行报废并扣减库存 ')
router.push(' / scrap / index ')
} catch (err: any) {
// 拦截器已弹后端业务错误;无响应才算网络异常
if (!err?.response) ElMessage.error(' 网络异常 , 请重试 ' )
} finally {
loading . value = false
}
}
onMounted ( ( ) => {
loadApprovalRequests ( )
} )
< / script >
< style scoped >
. app - container {
padding : 10 px ;
. app - container . mobile - optimized {
padding : 10 px ; max - width : 700 px ; margin : 0 auto ;
}
. box - card {
max - width : 1200 px ;
margin : 0 auto ;
}
. card - header { display : flex ; justify - content : space - between ; align - items : center ; }
. title - box { font - size : 16 px ; font - weight : bold ; display : flex ; align - items : center ; gap : 8 px ; }
. card - header {
/* 模式条 */
. mode - switch - bar {
display : flex ;
justify - content : space - between ;
align - items : center ;
gap : 16 px ;
margin - bottom : 16 px ;
padding : 12 px 16 px ;
background : # fef0f0 ;
border - radius : 8 px ;
border : 1 px solid # fde2e2 ;
}
. mode - hint { color : # 909399 ; font - size : 13 px ; }
. title - box {
/* 申请单选择 */
. approval - request - select { margin - bottom : 16 px ; }
. select - tip { margin : 6 px 0 0 0 ; color : # 909399 ; font - size : 12 px ; }
/* 待执行清单 */
. planned - items - section {
margin - bottom : 16 px ;
padding : 12 px ;
background : # fef0f0 ;
border : 1 px solid # fde2e2 ;
border - radius : 8 px ;
}
. planned - header {
display : flex ;
align - items : center ;
justify - content : space - between ;
margin - bottom : 10 px ;
}
. planned - title { font - weight : bold ; font - size : 14 px ; color : # F56C6C ; }
/* 扫码区 */
. scan - section { margin - bottom : 20 px ; }
. scan - progress {
background : # f5f7fa ;
border : 1 px solid # ebeef5 ;
border - radius : 6 px ;
padding : 10 px 14 px ;
margin - bottom : 12 px ;
}
. progress - info {
display : flex ;
align - items : center ;
gap : 10 px ;
font - size : 18 px ;
font - weight : bold ;
margin - bottom : 6 px ;
font - size : 13 px ;
font - weight : 600 ;
}
. scan - section {
margin - bottom : 20 px ;
}
. camera - placeholder {
display : fle x;
flex - direction : column ;
align - items : ce nter;
justify - content : center ;
height : 180 px ;
background : linear - gradient ( 135 deg , # ecf5ff 0 % , # d9ecff 100 % ) ;
border : 2 px dashed # 409 EFF ;
border - radius : 8 px ;
cursor : pointer ;
height : 120 px ; background : # f5f7fa ; border : 1 px dashed # dcdfe6 ; border - radius : 8 p x;
display : flex ; flex- direction : column ; justify - content : center ; align - items : center ;
color : # 909399 ; margin - bottom : 10 px ; cursor : poi nter;
transition : all 0.3 s ;
}
. camera - placeholder : active { background : # e6e8eb ; }
. camera - placeholder . text { margin - top : 5 px ; font - size : 13 px ; }
. camera - placeholder : hover {
background : linear - gradient ( 135 deg , # d9ecff 0 % , # b3d8ff 100 % ) ;
transform : scale ( 1.01 ) ;
}
. input - box : deep ( . el - input _ _wrapper ) { box - shadow : 0 0 0 1 px # dcdfe6 inset ; }
. camera - placeholder . text {
margin - top : 10 px ;
color : # 409 EFF ;
font - size : 14 px ;
}
. input - box {
margin - top : 15 px ;
}
. input - box : deep ( . el - input _ _wrapper ) {
box - shadow : 0 0 0 1 px # dcdfe6 inset ;
}
. input - box : deep ( . el - input _ _wrapper : hover ) {
box - shadow : 0 0 0 1 px # c0c4cc inset ;
}
. input - box : deep ( . el - input _ _wrapper . is - focus ) {
box - shadow : 0 0 0 1 px # 409 EFF inset ;
}
. cart - section {
margin - bottom : 20 px ;
}
. form - section {
margin - top : 20 px ;
padding - top : 20 px ;
border - top : 1 px solid # eee ;
}
. form - section : deep ( . el - divider _ _text ) {
font - weight : bold ;
color : # F56C6C ;
}
. bottom - actions {
display : flex ;
justify - content : flex - end ;
gap : 15 px ;
margin - top : 20 px ;
padding - top : 20 px ;
border - top : 1 px solid # eee ;
}
. bottom - actions . el - button -- large {
padding : 12 px 30 px ;
font - size : 16 px ;
}
/* 全屏扫码样式 */
/* 全屏扫码 */
. fullscreen - scanner - overlay {
position : fixed ;
top : 0 ;
left : 0 ;
right : 0 ;
bottom : 0 ;
background : # 000 ;
z - index : 2000 ;
display : flex ;
flex - direction : column ;
top : 0 ; left : 0 ; width : 100 vw ; height : 100 vh ;
background : # 000 ; z - index : 9999 ;
display : flex ; flex - direction : column ;
}
. scanner - header {
display : flex ;
align - items : center ;
justify - content : space - between ;
padding : 15 px ;
background : rgba ( 0 , 0 , 0 , 0.8 ) ;
color : # fff ;
height : 60 px ; display : flex ; align - items : center ; justify - content : space - between ;
padding : 0 15 px ; background : rgba ( 0 , 0 , 0 , 0.6 ) ; color : # fff ;
position : absolute ; top : 0 ; width : 100 % ; z - index : 10 ;
}
. scanner - title {
font - size : 18 px ;
font - weight : bold ;
}
. close - btn {
background : rgba ( 255 , 255 , 255 , 0.2 ) ;
border : none ;
color : # fff ;
}
. scanner - placeholder {
width : 40 px ;
}
. scanner - title { font - size : 16 px ; font - weight : bold ; }
. close - btn { background : rgba ( 255 , 255 , 255 , 0.2 ) ; border : none ; color : # fff ; }
. scanner - body {
flex : 1 ;
display : flex ;
align - items : center ;
justify - content : ce nter ;
flex : 1 ; width : 100 % ; position : relative ;
display : flex ; align - items : center ; justify - content : center ;
}
: deep ( . qr - scanner - co ntain er ) {
width : 100 % ! important ; height : 100 % ! important ; border - radius : 0 ! important ;
}
. scanner - footer {
padding : 2 0px ;
text - align : center ;
background : rgba ( 0 , 0 , 0 , 0.8 ) ;
color : # fff ;
position : absolute ; bottom : 0 ; width : 100 % ;
padding : 20 px ; background : rgba ( 0 , 0 , 0 , 0.6 ) ; color : # fff ;
text - align : center ; z - index : 10 ;
}
. current - count { color : # F56C6C ; font - weight : bold ; margin - top : 5 px ; font - size : 16 px ; }
. scanner - footer p {
margin : 5 px 0 ;
}
/* 表单与购物车 */
. cart - section { margin - bottom : 20 px ; }
. form - section { background : # fff ; }
. form - section : deep ( . el - divider _ _text ) { font - weight : bold ; color : # F56C6C ; }
. current - count {
font - size : 16 px ;
font - weight : bold ;
color : # F56C6C ;
}
. bottom - actions { display : flex ; justify - content : space - between ; margin - top : 20 px ; }
. bottom - actions . el - button { width : 48 % ; }
@ media ( max - width : 768 px ) {
. app - container {
padding : 5 px ;
}
. title - box {
font - size : 16 px ;
}
. camera - placeholder {
height : 120 px ;
}
. bottom - actions {
flex - direction : column ;
}
. bottom - actions . el - button {
width : 100 % ;
}
@ media screen and ( max - width : 768 px ) {
. title - box { font - size : 14 px ; }
. camera - placeholder { height : 100 px ; }
}
< / style >