<template>
|
<div class="picking-container" v-loading="globalLoading" element-loading-text="处理中..."
|
element-loading-background="rgba(255, 255, 255, 0.8)" element-loading-spinner="el-icon-loading"
|
element-loading-custom-class="custom-loading">
|
<!-- 顶部订单信息 -->
|
<el-card class="order-info-card" shadow="never">
|
<div class="order-header">
|
<div class="order-title">
|
<i class="el-icon-document"></i>
|
<span class="order-label">订单号:</span>
|
<span class="order-value">{{ orderNo }}</span>
|
</div>
|
<div class="order-status">
|
<el-tag v-if="orderInfo" :type="getStatusType(orderInfo.orderStatus)" size="medium"
|
style="margin-left: 10px;">
|
{{ orderInfo.statusName || '进行中' }}
|
</el-tag>
|
</div>
|
</div>
|
</el-card>
|
|
<!-- 扫码操作区域 -->
|
<el-card class="scan-section-card" shadow="never">
|
<div class="scan-section">
|
<el-alert title="扫描托盘码可进行回库,扫描物料条码进行拣选" type="info" :closable="false" show-icon class="scan-alert">
|
<template #default>
|
<div>
|
<div style="margin-top: 8px; font-size: 13px; color: #666;">
|
<i class="el-icon-info" style="color: #409EFF;"></i>
|
扫描条码后会自动请求拣选接口,无需手动点击确认
|
</div>
|
</div>
|
</template>
|
</el-alert>
|
|
<el-form :model="scanForm" :rules="scanRules" ref="scanFormRef" class="scan-form">
|
<el-row :gutter="20">
|
<el-col :span="12">
|
<el-form-item label="托盘码" prop="palletCode">
|
<el-input ref="palletInput" v-model="scanForm.palletCode" placeholder="请扫描托盘码" size="large"
|
clearable @keyup.enter="handlePalletScan">
|
<template #prefix>
|
<i class="el-icon-box"></i>
|
</template>
|
</el-input>
|
</el-form-item>
|
</el-col>
|
<el-col :span="12">
|
<el-form-item label="物料条码" prop="materialBarcode">
|
<el-input ref="materialInput" v-model="scanForm.materialBarcode" placeholder="请扫描物料条码"
|
size="large" clearable :disabled="!scanForm.palletCode"
|
@keyup.enter="handleMaterialScan">
|
<template #prefix>
|
<i class="el-icon-s-grid"></i>
|
</template>
|
<template #append>
|
<el-button type="primary" @click="handleMaterialScan" :loading="pickLoading">
|
扫描拣选
|
</el-button>
|
</template>
|
</el-input>
|
</el-form-item>
|
</el-col>
|
</el-row>
|
|
<!-- 操作按钮 -->
|
<el-row :gutter="20" style="margin-top: 10px;">
|
<el-col :span="24">
|
<div class="action-buttons">
|
<el-button type="success" size="large" @click="handleReturnToWarehouse"
|
:disabled="!scanForm.palletCode" :loading="returnLoading">
|
<i class="el-icon-refresh-left"></i>
|
回库
|
</el-button>
|
</div>
|
</el-col>
|
</el-row>
|
</el-form>
|
</div>
|
</el-card>
|
|
<!-- 扫描验证的条码列表 -->
|
<div class="tables-section">
|
<el-card class="table-card" shadow="never">
|
<template #header>
|
<div class="card-header">
|
<span class="card-title">
|
<i class="el-icon-s-grid"></i>
|
扫描通过验证的条码列表
|
</span>
|
<div>
|
<el-badge :value="scannedList.length" class="badge-item" type="primary">
|
<el-button size="small" @click="clearScannedList" icon="el-icon-delete">
|
清空列表
|
</el-button>
|
</el-badge>
|
<el-button size="small" @click="refreshScannedList" icon="el-icon-refresh" style="margin-left: 10px;">
|
刷新
|
</el-button>
|
</div>
|
</div>
|
</template>
|
|
<el-table :data="scannedList" height="500" stripe>
|
<el-table-column type="index" label="序号" width="60" align="center" />
|
<el-table-column prop="barcode" label="条码" width="200">
|
<template #default="scope">
|
<el-tag type="success">{{ scope.row.barcode }}</el-tag>
|
</template>
|
</el-table-column>
|
<el-table-column prop="scanTime" label="扫描时间" width="160">
|
<template #default="scope">
|
<el-text type="info">{{ scope.row.scanTime }}</el-text>
|
</template>
|
</el-table-column>
|
<el-table-column prop="status" label="状态" width="80" align="center">
|
<template #default="scope">
|
<el-tag :type="scope.row.status === 'success' ? 'success' : 'danger'" size="small">
|
{{ scope.row.status === 'success' ? '成功' : '失败' }}
|
</el-tag>
|
</template>
|
</el-table-column>
|
<el-table-column prop="message" label="消息" show-overflow-tooltip />
|
<el-table-column label="操作" width="80" align="center">
|
<template #default="scope">
|
<el-button type="text" size="small" @click="removeScannedItem(scope.row.barcode)" :disabled="scope.row.status === 'pending'">
|
删除
|
</el-button>
|
</template>
|
</el-table-column>
|
</el-table>
|
|
<div class="table-footer">
|
<el-descriptions :column="4" size="small">
|
<el-descriptions-item label="总条码数">
|
<el-text type="info">{{ scannedList.length }}</el-text>
|
</el-descriptions-item>
|
<el-descriptions-item label="成功数">
|
<el-text type="success">{{ successCount }}</el-text>
|
</el-descriptions-item>
|
<el-descriptions-item label="失败数">
|
<el-text type="danger">{{ failedCount }}</el-text>
|
</el-descriptions-item>
|
<el-descriptions-item label="重复数">
|
<el-text type="warning">{{ duplicateCount }}</el-text>
|
</el-descriptions-item>
|
</el-descriptions>
|
</div>
|
</el-card>
|
</div>
|
|
<!-- 确认对话框 -->
|
<el-dialog v-model="confirmDialogVisible" title="操作确认" width="400px" :before-close="handleDialogClose">
|
<div class="confirm-content">
|
<p>{{ confirmMessage }}</p>
|
</div>
|
<template #footer>
|
<span class="dialog-footer">
|
<el-button @click="confirmDialogVisible = false">取消</el-button>
|
<el-button type="primary" @click="executeConfirm" :loading="executeLoading">
|
确定
|
</el-button>
|
</span>
|
</template>
|
</el-dialog>
|
</div>
|
</template>
|
|
<script>
|
import { stationManager } from "@/../src/uitils/stationManager";
|
import { ElLoading } from 'element-plus'
|
|
export default {
|
name: 'OutPicking',
|
data() {
|
return {
|
orderNo: '',
|
orderInfo: null,
|
scanForm: {
|
palletCode: '',
|
materialBarcode: ''
|
},
|
scanRules: {
|
palletCode: [
|
{ required: true, message: '请扫描托盘码', trigger: 'blur' }
|
]
|
},
|
// 扫描通过的条码列表
|
scannedList: [],
|
confirmDialogVisible: false,
|
confirmMessage: '',
|
currentAction: null,
|
executeLoading: false,
|
globalLoading: false,
|
loadingInstance: null,
|
pickLoading: false,
|
returnLoading: false,
|
// 托盘码正则表达式
|
palletCodeRegex: /^[A-Z]\d{9}$/
|
}
|
},
|
computed: {
|
// 计算成功数量
|
successCount() {
|
return this.scannedList.filter(item => item.status === 'success').length;
|
},
|
// 计算失败数量
|
failedCount() {
|
return this.scannedList.filter(item => item.status === 'failed').length;
|
},
|
// 计算重复扫描次数(不包括当前列表中已经存在的)
|
duplicateCount() {
|
const barcodes = this.scannedList.map(item => item.barcode);
|
const uniqueBarcodes = [...new Set(barcodes)];
|
return barcodes.length - uniqueBarcodes.length;
|
}
|
},
|
mounted() {
|
this.initPage()
|
},
|
methods: {
|
initPage() {
|
// 从路由参数获取订单号
|
this.orderNo = this.$route.query.orderNo || ''
|
if (!this.orderNo) {
|
this.$message.error('订单号不能为空')
|
this.$router.back()
|
return
|
}
|
|
// 加载订单信息
|
this.loadOrderInfo()
|
|
// 自动聚焦到托盘码输入框
|
this.$nextTick(() => {
|
if (this.$refs.palletInput) {
|
this.$refs.palletInput.focus()
|
}
|
})
|
},
|
async loadOrderInfo() {
|
try {
|
this.showFullScreenLoading()
|
const response = await this.http.get(`/api/Outbound/GetOrderInfo?orderNo=${this.orderNo}`)
|
if (response.status) {
|
this.orderInfo = response.data
|
} else {
|
this.$message.error(response.message || '加载订单信息失败')
|
}
|
}
|
catch (error) {
|
this.$message.error('加载订单信息失败')
|
} finally {
|
this.hideFullScreenLoading()
|
}
|
},
|
|
async handlePalletScan() {
|
if (!this.scanForm.palletCode) {
|
this.$message.warning('请输入托盘码')
|
return
|
}
|
|
// 验证托盘码格式
|
if (!this.palletCodeRegex.test(this.scanForm.palletCode)) {
|
this.$message.error('托盘码格式错误!正确格式应为:大写字母开头 + 9位数字(如A123456789)')
|
// 验证失败:清空托盘码输入框并保持聚焦,方便连续扫描
|
this.scanForm.palletCode = ''
|
this.$nextTick(() => {
|
this.$refs.palletInput.focus()
|
})
|
return
|
}
|
|
// 验证通过:清空物料条码并聚焦到物料条码输入框
|
this.scanForm.materialBarcode = ''
|
this.$nextTick(() => {
|
if (this.$refs.materialInput) {
|
this.$refs.materialInput.focus()
|
}
|
})
|
this.$message.success('托盘码验证通过,请扫描物料条码')
|
},
|
|
async handleMaterialScan() {
|
if (!this.scanForm.palletCode) {
|
this.$message.warning('请先扫描托盘码')
|
this.$refs.palletInput.focus()
|
return
|
}
|
|
if (!this.scanForm.materialBarcode) {
|
this.$message.warning('请扫描物料条码')
|
return
|
}
|
|
// 1. 检查是否重复扫描
|
const isDuplicate = this.scannedList.some(item => item.barcode === this.scanForm.materialBarcode);
|
if (isDuplicate) {
|
this.$message.warning(`条码 ${this.scanForm.materialBarcode} 已扫描过,不能重复扫描`)
|
this.scanForm.materialBarcode = ''
|
this.$nextTick(() => {
|
if (this.$refs.materialInput) {
|
this.$refs.materialInput.focus()
|
}
|
})
|
return
|
}
|
|
this.pickLoading = true
|
|
// 2. 先添加到列表,状态为pending(处理中)
|
const pendingItem = {
|
barcode: this.scanForm.materialBarcode,
|
materielCode: '',
|
materielName: '',
|
batchNo: '',
|
quantity: 1,
|
unit: '',
|
scanTime: this.formatTime(new Date()),
|
status: 'pending',
|
message: '处理中...'
|
};
|
this.scannedList.unshift(pendingItem); // 添加到列表顶部
|
|
try {
|
const response = await this.http.post('/api/Outbound/RecheckPicking', {
|
orderNo: this.orderNo,
|
barCode: this.scanForm.materialBarcode
|
})
|
|
// 3. 更新列表中的状态
|
const index = this.scannedList.findIndex(item => item.barcode === this.scanForm.materialBarcode);
|
if (index !== -1) {
|
if (response.status) {
|
// 成功
|
this.scannedList[index] = {
|
...this.scannedList[index],
|
...response.data, // 假设后端返回了物料信息
|
status: 'success',
|
message: '拣选成功'
|
};
|
this.$message.success('拣选成功')
|
} else {
|
// 失败
|
this.scannedList[index] = {
|
...this.scannedList[index],
|
status: 'failed',
|
message: response.message || '拣选失败'
|
};
|
this.$message.error(response.message || '拣选失败')
|
}
|
}
|
|
} catch (error) {
|
// 更新失败状态
|
const index = this.scannedList.findIndex(item => item.barcode === this.scanForm.materialBarcode);
|
if (index !== -1) {
|
this.scannedList[index] = {
|
...this.scannedList[index],
|
status: 'failed',
|
message: '请求失败'
|
};
|
}
|
this.$message.error('拣选失败')
|
} finally {
|
// 清空条码框
|
this.scanForm.materialBarcode = ''
|
this.pickLoading = false
|
|
// 重新聚焦到物料条码输入框,方便继续扫描
|
this.$nextTick(() => {
|
if (this.$refs.materialInput) {
|
this.$refs.materialInput.focus()
|
}
|
})
|
}
|
},
|
|
handleReturnToWarehouse() {
|
if (!this.scanForm.palletCode) {
|
this.$message.warning('请先扫描托盘码')
|
return
|
}
|
|
// 额外验证:回库操作前也验证托盘码格式
|
if (!this.palletCodeRegex.test(this.scanForm.palletCode)) {
|
this.$message.error('托盘码格式错误!正确格式应为:大写字母开头 + 9位数字(如A123456789)')
|
this.scanForm.palletCode = ''
|
this.$nextTick(() => {
|
this.$refs.palletInput.focus()
|
})
|
return
|
}
|
|
this.confirmMessage = `确定要将托盘 ${this.scanForm.palletCode} 回库吗?`
|
this.currentAction = 'returnToWarehouse'
|
this.confirmDialogVisible = true
|
},
|
|
async executeConfirm() {
|
if (this.currentAction === 'returnToWarehouse') {
|
await this.executeReturnToWarehouse()
|
}
|
},
|
|
async executeReturnToWarehouse() {
|
this.executeLoading = true
|
this.returnLoading = true
|
|
try {
|
const response = await this.http.post('/api/Outbound/ReturnToWarehouse', {
|
orderNo: this.orderNo,
|
palletCode: this.scanForm.palletCode,
|
station: stationManager.getStation()
|
})
|
|
if (response.status) {
|
this.$message.success('回库成功')
|
this.confirmDialogVisible = false
|
this.resetForm()
|
} else {
|
this.$message.error(response.message || '回库失败')
|
}
|
} catch (error) {
|
this.$message.error('回库失败')
|
} finally {
|
this.executeLoading = false
|
this.returnLoading = false
|
}
|
},
|
|
handleDialogClose() {
|
if (!this.executeLoading) {
|
this.confirmDialogVisible = false
|
}
|
},
|
|
// 刷新扫描列表(主要是为了重新获取后端状态)
|
refreshScannedList() {
|
// 可以在这里重新验证列表中条码的状态
|
this.$message.info('列表已刷新')
|
},
|
|
// 清空扫描列表
|
clearScannedList() {
|
this.$confirm('确定要清空扫描列表吗?', '提示', {
|
type: 'warning'
|
}).then(() => {
|
this.scannedList = []
|
this.$message.success('列表已清空')
|
}).catch(() => {
|
// 用户取消
|
})
|
},
|
|
// 删除单个扫描记录
|
removeScannedItem(barcode) {
|
this.scannedList = this.scannedList.filter(item => item.barcode !== barcode)
|
this.$message.success('已删除')
|
},
|
|
resetForm() {
|
this.scanForm.palletCode = ''
|
this.scanForm.materialBarcode = ''
|
this.$nextTick(() => {
|
if (this.$refs.palletInput) {
|
this.$refs.palletInput.focus()
|
}
|
})
|
},
|
|
// 格式化时间
|
formatTime(date) {
|
const year = date.getFullYear()
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
const day = String(date.getDate()).padStart(2, '0')
|
const hours = String(date.getHours()).padStart(2, '0')
|
const minutes = String(date.getMinutes()).padStart(2, '0')
|
const seconds = String(date.getSeconds()).padStart(2, '0')
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
},
|
|
getStatusType(status) {
|
const statusMap = {
|
0: 'info', // 待处理
|
1: 'warning', // 进行中
|
20: 'primary', // 拣选中
|
30: 'success', // 已完成
|
40: 'danger' // 异常
|
}
|
return statusMap[status] || 'info'
|
},
|
|
// 显示全屏遮罩层
|
showFullScreenLoading() {
|
if (this.loadingInstance) {
|
this.loadingInstance.close()
|
}
|
this.loadingInstance = ElLoading.service({
|
lock: true,
|
text: '处理中...',
|
background: 'rgba(0, 0, 0, 0.7)',
|
customClass: 'custom-full-loading'
|
})
|
},
|
|
// 隐藏全屏遮罩层
|
hideFullScreenLoading() {
|
if (this.loadingInstance) {
|
this.loadingInstance.close()
|
this.loadingInstance = null
|
}
|
}
|
}
|
}
|
</script>
|
|
<style scoped>
|
.picking-container {
|
padding: 20px;
|
background-color: #f5f5f5;
|
min-height: 100vh;
|
position: relative;
|
overflow: hidden;
|
}
|
|
/* 订单信息卡片 */
|
.order-info-card {
|
margin-bottom: 20px;
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
color: white;
|
}
|
|
.order-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
}
|
|
.order-title {
|
display: flex;
|
align-items: center;
|
font-size: 18px;
|
font-weight: bold;
|
}
|
|
.order-title i {
|
margin-right: 10px;
|
font-size: 24px;
|
}
|
|
.order-label {
|
margin-left: 10px;
|
opacity: 0.9;
|
}
|
|
.order-value {
|
font-size: 20px;
|
font-weight: bold;
|
letter-spacing: 1px;
|
}
|
|
/* 扫码区域 */
|
.scan-section-card {
|
margin-bottom: 20px;
|
}
|
|
.scan-section {
|
padding: 10px 0;
|
}
|
|
.scan-alert {
|
margin-bottom: 20px;
|
}
|
|
.scan-form {
|
margin-top: 20px;
|
}
|
|
.action-buttons {
|
display: flex;
|
gap: 8px;
|
justify-content: center;
|
}
|
|
.action-buttons .el-button {
|
flex: 0 0 auto;
|
width: 150px;
|
height: 40px;
|
font-weight: bold;
|
border-radius: 6px;
|
}
|
|
/* 表格区域 */
|
.tables-section {
|
margin-top: 20px;
|
}
|
|
.table-card {
|
height: 600px;
|
display: flex;
|
flex-direction: column;
|
}
|
|
.card-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
}
|
|
.card-title {
|
display: flex;
|
align-items: center;
|
font-weight: bold;
|
font-size: 16px;
|
}
|
|
.card-title i {
|
margin-right: 8px;
|
color: #409EFF;
|
}
|
|
.badge-item {
|
margin-left: 10px;
|
}
|
|
.table-footer {
|
margin-top: 10px;
|
padding-top: 10px;
|
border-top: 1px solid #ebeef5;
|
}
|
|
/* 表格行样式 */
|
.el-table tbody tr:hover>td {
|
background-color: #f0f9ff !important;
|
}
|
|
.el-table tbody tr.current-row>td {
|
background-color: #e1f3ff !important;
|
}
|
|
/* 对话框样式 */
|
.confirm-content {
|
padding: 20px 0;
|
text-align: center;
|
}
|
|
.confirm-content p {
|
font-size: 16px;
|
color: #606266;
|
}
|
|
.dialog-footer {
|
text-align: center;
|
}
|
|
/* 描述列表样式 */
|
::v-deep .el-descriptions__label {
|
font-weight: bold;
|
color: #909399;
|
}
|
|
::v-deep .el-descriptions__content {
|
color: #606266;
|
}
|
|
/* Element Plus Loading 遮罩层样式修复 */
|
::v-deep .custom-loading {
|
background-color: rgba(0, 0, 0, 0.7) !important;
|
z-index: 9999 !important;
|
}
|
|
::v-deep .custom-loading .el-loading-mask {
|
background-color: rgba(0, 0, 0, 0.7) !important;
|
}
|
|
::v-deep .custom-loading .el-loading-spinner {
|
z-index: 10000 !important;
|
}
|
|
::v-deep .custom-loading .el-loading-text {
|
color: #ffffff !important;
|
font-weight: bold !important;
|
font-size: 16px !important;
|
}
|
|
/* 全屏Loading自定义样式 */
|
::v-deep .custom-full-loading {
|
z-index: 999999 !important;
|
}
|
|
::v-deep .custom-full-loading .el-loading-mask {
|
z-index: 999999 !important;
|
}
|
|
::v-deep .custom-full-loading .el-loading-spinner {
|
z-index: 1000000 !important;
|
}
|
|
::v-deep .custom-full-loading .el-loading-text {
|
color: #ffffff !important;
|
font-weight: bold !important;
|
font-size: 18px !important;
|
}
|
|
/* 确保对话框不会遮盖loading */
|
::v-deep .el-dialog {
|
z-index: 2000 !important;
|
}
|
|
::v-deep .el-dialog__wrapper {
|
z-index: 2000 !important;
|
}
|
|
/* 确保容器相对定位 */
|
.picking-container {
|
position: relative !important;
|
min-height: 100vh;
|
}
|
|
/* 输入框附加按钮样式 */
|
::v-deep .el-input-group__append {
|
background-color: #409EFF !important;
|
border-color: #409EFF !important;
|
}
|
|
::v-deep .el-input-group__append .el-button {
|
color: white !important;
|
background-color: transparent !important;
|
border: none !important;
|
}
|
|
/* 表格样式调整 */
|
::v-deep .el-table th {
|
background-color: #fafafa;
|
font-weight: 600;
|
color: #303133;
|
}
|
|
::v-deep .el-table .el-text {
|
font-weight: 500;
|
}
|
|
/* 标签样式增强 */
|
::v-deep .el-tag--small {
|
font-weight: 500;
|
}
|
|
/* 提示信息样式增强 */
|
.scan-alert ::v-deep .el-alert__content {
|
width: 100%;
|
}
|
|
.scan-alert ::v-deep .el-alert__description {
|
margin-top: 8px;
|
}
|
|
/* Element UI 组件样式覆盖 */
|
::v-deep .el-card__header {
|
padding: 18px 20px;
|
border-bottom: 1px solid #ebeef5;
|
}
|
|
::v-deep .el-input__inner {
|
border-radius: 6px;
|
}
|
|
::v-deep .el-button--primary {
|
background: linear-gradient(135deg, #409EFF 0%, #3a8ee6 100%);
|
border: none;
|
}
|
|
::v-deep .el-button--success {
|
background: linear-gradient(135deg, #67C23A 0%, #5daf34 100%);
|
border: none;
|
}
|
|
/* 图标样式 */
|
.el-icon-document,
|
.el-icon-box,
|
.el-icon-s-grid,
|
.el-icon-refresh-left,
|
.el-icon-time,
|
.el-icon-circle-check {
|
font-size: 18px;
|
}
|
</style>
|