<template>
|
<div class="picking-container">
|
<!-- 顶部订单信息 -->
|
<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" v-if="orderInfo">
|
<el-tag :type="getStatusType(orderInfo.status)" size="medium">
|
{{ 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>
|
<span>1. 请先扫描托盘码 → 2. 再扫描物料条码</span>
|
</template>
|
</el-alert>
|
|
<el-form :model="scanForm" :rules="scanRules" ref="scanFormRef" class="scan-form">
|
<el-row :gutter="20">
|
<el-col :span="8">
|
<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="8">
|
<el-form-item label="物料条码" prop="materialBarcode">
|
<el-input ref="materialInput" v-model="scanForm.materialBarcode" placeholder="请扫描物料条码"
|
size="large" clearable :disabled="unpickedData.length <= 0"
|
@keyup.enter="handleMaterialScan">
|
<template #prefix>
|
<i class="el-icon-s-grid"></i>
|
</template>
|
</el-input>
|
</el-form-item>
|
</el-col>
|
<el-col :span="8">
|
<el-form-item class="button-form-item">
|
<div class="action-buttons">
|
<el-button type="primary" size="large" @click="handleConfirmPick"
|
:loading="confirmLoading" :disabled="!canConfirm">
|
<i class="el-icon-check"></i>
|
确认拣选
|
</el-button>
|
<el-button type="warning" size="large" @click="handleEmptyBox"
|
:disabled="!scanForm.palletCode">
|
<i class="el-icon-delete"></i>
|
取空箱
|
</el-button>
|
<el-button type="success" size="large" @click="handleReturnToWarehouse"
|
:disabled="!scanForm.palletCode">
|
<i class="el-icon-refresh-left"></i>
|
回库
|
</el-button>
|
</div>
|
</el-form-item>
|
</el-col>
|
</el-row>
|
</el-form>
|
</div>
|
</el-card>
|
|
<!-- 数据列表区域 -->
|
<div class="tables-section">
|
<el-row :gutter="20">
|
<!-- 未拣选列表 -->
|
<el-col :span="12">
|
<el-card class="table-card" shadow="never">
|
<template #header>
|
<div class="card-header">
|
<span class="card-title">
|
<i class="el-icon-time"></i>
|
未拣选列表
|
</span>
|
<el-badge :value="unpickedCount" class="badge-item" type="warning">
|
<el-button size="small" @click="refreshUnpickedTable" icon="el-icon-refresh">
|
刷新
|
</el-button>
|
</el-badge>
|
</div>
|
</template>
|
|
<el-table ref="unpickedTable" :data="unpickedData" height="400" stripe highlight-current-row>
|
<el-table-column type="index" label="序号" width="60" align="center" />
|
<el-table-column prop="materielCode" label="物料编码" width="120" show-overflow-tooltip />
|
<el-table-column prop="materielName" label="物料名称" width="80" show-overflow-tooltip />
|
<el-table-column prop="batchNo" label="批次号" width="100" />
|
<el-table-column prop="assignQuantity" label="分拣数量" width="80" align="right">
|
<template #default="scope">
|
<el-text type="danger">{{ scope.row.assignQuantity }}</el-text>
|
</template>
|
</el-table-column>
|
<el-table-column prop="sortedQuantity" label="已分拣数量" width="120" align="right">
|
<template #default="scope">
|
<el-text type="danger">{{ scope.row.sortedQuantity }}</el-text>
|
</template>
|
</el-table-column>
|
<el-table-column prop="unit" label="单位" width="60" />
|
<el-table-column prop="locationCode" label="库位" />
|
<!-- <el-table-column label="操作" width="80" align="center">
|
<template #default="scope">
|
<el-button type="text" size="small" @click="quickPick(scope.row)"
|
:disabled="!scanForm.palletCode">
|
拣选
|
</el-button>
|
</template>
|
</el-table-column> -->
|
</el-table>
|
|
<div class="table-footer">
|
<el-descriptions :column="2" size="small">
|
<el-descriptions-item label="总条数">
|
<el-text type="info">{{ unpickedTotal }}</el-text>
|
</el-descriptions-item>
|
<el-descriptions-item label="总数量">
|
<el-text type="warning">{{ unpickedQuantity }}</el-text>
|
</el-descriptions-item>
|
</el-descriptions>
|
</div>
|
</el-card>
|
</el-col>
|
|
<!-- 已拣选列表 -->
|
<el-col :span="12">
|
<el-card class="table-card" shadow="never">
|
<template #header>
|
<div class="card-header">
|
<span class="card-title">
|
<i class="el-icon-circle-check"></i>
|
已拣选列表
|
</span>
|
<el-badge :value="pickedCount" class="badge-item" type="success">
|
<el-button size="small" @click="refreshPickedTable" icon="el-icon-refresh">
|
刷新
|
</el-button>
|
</el-badge>
|
</div>
|
</template>
|
|
<el-table ref="pickedTable" :data="pickedData" height="400" stripe>
|
<el-table-column type="index" label="序号" width="60" align="center" />
|
<el-table-column prop="materielCode" label="物料编码" width="120" />
|
<el-table-column prop="materielName" label="物料名称" show-overflow-tooltip />
|
<el-table-column prop="batchNo" label="批次号" width="100" />
|
<el-table-column prop="changeQuantity" label="拣选数量" width="80" align="right">
|
<template #default="scope">
|
<el-text type="success">{{ 0 - scope.row.changeQuantity }}</el-text>
|
</template>
|
</el-table-column>
|
<el-table-column prop="changeQuantity" label="原库存量" width="80" align="right">
|
<template #default="scope">
|
<el-text type="success">{{ scope.row.beforeQuantity }}</el-text>
|
</template>
|
</el-table-column>
|
<el-table-column prop="changeQuantity" label="拣选后库存量" width="80" align="right">
|
<template #default="scope">
|
<el-text type="success">{{ scope.row.afterQuantity }}</el-text>
|
</template>
|
</el-table-column>
|
<el-table-column prop="unit" label="单位" width="60" />
|
<el-table-column prop="palletCode" label="托盘码" width="100" />
|
<el-table-column prop="createDate" label="拣选时间" width="160" />
|
<el-table-column prop="originalBarcode" label="原物料码" width="160" />
|
<el-table-column prop="newBarcode" label="新物料码" width="160" />
|
<!-- <el-table-column label="操作" width="80" align="center">
|
<template #default="scope">
|
<el-button type="text" size="small" @click="undoPick(scope.row)">
|
撤销
|
</el-button>
|
</template>
|
</el-table-column> -->
|
</el-table>
|
|
<div class="table-footer">
|
<el-descriptions :column="2" size="small">
|
<el-descriptions-item label="总条数">
|
<el-text type="info">{{ pickedTotal }}</el-text>
|
</el-descriptions-item>
|
<el-descriptions-item label="总数量">
|
<el-text type="success">{{ pickedQuantity }}</el-text>
|
</el-descriptions-item>
|
</el-descriptions>
|
</div>
|
</el-card>
|
</el-col>
|
</el-row>
|
</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>
|
export default {
|
name: 'OutPicking',
|
data() {
|
return {
|
orderNo: '',
|
orderInfo: null,
|
scanForm: {
|
palletCode: '',
|
materialBarcode: ''
|
},
|
scanRules: {
|
palletCode: [
|
{ required: true, message: '请扫描托盘码', trigger: 'blur' }
|
],
|
materialBarcode: [
|
{ required: true, message: '请扫描物料条码', trigger: 'blur' }
|
]
|
},
|
unpickedData: [],
|
pickedData: [],
|
unpickedCount: 0,
|
unpickedTotal: 0,
|
unpickedQuantity: 0,
|
pickedCount: 0,
|
pickedTotal: 0,
|
pickedQuantity: 0,
|
confirmLoading: false,
|
confirmDialogVisible: false,
|
confirmMessage: '',
|
currentAction: null,
|
executeLoading: false
|
}
|
},
|
computed: {
|
canConfirm() {
|
return this.scanForm.palletCode && this.scanForm.materialBarcode
|
}
|
},
|
mounted() {
|
this.initPage()
|
},
|
methods: {
|
initPage() {
|
// 从路由参数获取订单号
|
this.orderNo = this.$route.query.orderNo || ''
|
if (!this.orderNo) {
|
this.$message.error('订单号不能为空')
|
this.$router.back()
|
return
|
}
|
// 自动聚焦到托盘码输入框
|
this.$nextTick(() => {
|
if (this.$refs.palletInput) {
|
this.$refs.palletInput.focus()
|
}
|
})
|
},
|
|
loadPalletData() {
|
if (!this.scanForm.palletCode) {
|
this.unpickedData = []
|
return
|
}
|
|
try {
|
this.loadUnpickedData();
|
this.loadPickedData();
|
|
} catch (error) {
|
console.error('加载托盘数据失败:', error)
|
this.unpickedData = []
|
}
|
},
|
loadUnpickedData() {
|
try {
|
this.http.post(`/api/Outbound/QueryPickingTasks?orderNo=${this.orderNo}&palletCode=${this.scanForm.palletCode}`, {}).then(response => {
|
if (response.status) {
|
if (response.data.length > 0) {
|
this.unpickedData = response.data
|
this.calculateUnpickedStats()
|
|
// 自动聚焦到物料条码输入框
|
this.$nextTick(() => {
|
if (this.$refs.materialInput) {
|
this.$refs.materialInput.focus()
|
}
|
})
|
} else {
|
this.$message.warning('该托盘无未拣选任务')
|
this.unpickedData = []
|
}
|
} else {
|
this.$message.error(response.message || '获取托盘数据失败')
|
this.unpickedData = []
|
}
|
}
|
)
|
|
} catch (error) {
|
console.error('加载未拣选数据失败:', error)
|
}
|
},
|
|
loadPickedData() {
|
try {
|
this.http.post(`/api/Outbound/QueryPickedList?orderNo=${this.orderNo}&palletCode=${this.scanForm.palletCode}`, {}).then(response => {
|
if (response.status) {
|
if (response.data.length > 0) {
|
this.pickedData = response.data
|
this.calculatePickedStats()
|
} else {
|
this.$message.warning('该托盘无未拣选任务')
|
this.unpickedData = []
|
}
|
} else {
|
this.$message.error(response.message || '获取托盘数据失败')
|
this.unpickedData = []
|
}
|
}
|
)
|
|
} catch (error) {
|
console.error('加载已拣选数据失败:', error)
|
}
|
},
|
|
// 计算未拣选
|
calculateUnpickedStats() {
|
// 未拣选条目数量
|
this.unpickedCount = this.unpickedData.length
|
|
// 计算未拣选的总数量(分拣数量 - 已分拣数量)
|
this.unpickedQuantity = this.unpickedData.reduce((sum, item) => {
|
const assignQty = item.assignQuantity || 0
|
const sortedQty = item.sortedQuantity || 0
|
const remainingQty = Math.max(0, assignQty - sortedQty)
|
return sum + remainingQty
|
}, 0)
|
|
// 总条目数(与条目数量相同,这里保留变量一致性)
|
this.unpickedTotal = this.unpickedCount
|
},
|
|
// 计算已拣选
|
calculatePickedStats() {
|
// 已拣选条目数量
|
this.pickedCount = this.pickedData.length
|
|
// 计算已拣选的总数量
|
this.pickedQuantity = 0 - this.pickedData.reduce((sum, item) => {
|
return (sum + item.changeQuantity)
|
}, 0)
|
|
// 总条目数(与条目数量相同)
|
this.pickedTotal = this.pickedCount
|
},
|
|
handlePalletScan() {
|
if (this.scanForm.palletCode) {
|
// this.$message.success(`托盘码: ${this.scanForm.palletCode}`)
|
this.loadPalletData()
|
|
|
}
|
},
|
|
handleMaterialScan() {
|
if (!this.scanForm.palletCode) {
|
this.$message.warning('请先扫描托盘码')
|
this.$refs.palletInput.focus()
|
return
|
}
|
|
if (!this.scanForm.materialBarcode) {
|
this.$message.warning('请扫描物料条码')
|
return
|
}
|
|
// 自动执行确认拣选
|
this.handleConfirmPick()
|
},
|
|
handleConfirmPick() {
|
if (!this.scanForm.palletCode || !this.scanForm.materialBarcode) {
|
this.$message.warning('请先扫描托盘码和物料条码')
|
return
|
}
|
|
this.confirmLoading = true
|
|
try {
|
this.http.post('/api/Outbound/CompleteOutboundWithBarcode', {
|
orderNo: this.orderNo,
|
palletCode: this.scanForm.palletCode,
|
barcode: this.scanForm.materialBarcode,
|
operator: this.getUserName()
|
}).then(response => {
|
if (response.status) {
|
this.$message.success('拣选确认成功')
|
this.resetMaterialBarcode()
|
// this.loadUnpickedData()
|
// this.loadPickedData()
|
this.loadPalletData()
|
} else {
|
this.$message.error(response.message || '拣选确认失败')
|
}
|
})
|
} catch (error) {
|
console.error('拣选确认失败:', error)
|
this.$message.error('拣选确认失败')
|
} finally {
|
this.confirmLoading = false
|
}
|
},
|
|
handleEmptyBox() {
|
if (!this.scanForm.palletCode) {
|
this.$message.warning('请先扫描托盘码')
|
return
|
}
|
|
this.confirmMessage = `确定要取空托盘 ${this.scanForm.palletCode} 吗?`
|
this.currentAction = 'emptyBox'
|
this.confirmDialogVisible = true
|
},
|
|
handleReturnToWarehouse() {
|
if (!this.scanForm.palletCode) {
|
this.$message.warning('请先扫描托盘码')
|
return
|
}
|
|
this.confirmMessage = `确定要将托盘 ${this.scanForm.palletCode} 回库吗?`
|
this.currentAction = 'returnToWarehouse'
|
this.confirmDialogVisible = true
|
},
|
|
executeConfirm() {
|
this.executeLoading = true
|
|
try {
|
let apiUrl = ''
|
let params = {
|
orderNo: this.orderNo,
|
palletCode: this.scanForm.palletCode
|
}
|
|
if (this.currentAction === 'emptyBox') {
|
apiUrl = '/api/Outbound/EmptyBox'
|
} else if (this.currentAction === 'returnToWarehouse') {
|
apiUrl = '/api/Outbound/ReturnToWarehouse'
|
}
|
|
this.http.post(apiUrl, params).then(response => {
|
|
if (response.status) {
|
this.$message.success('操作成功')
|
this.confirmDialogVisible = false
|
this.resetForm()
|
// this.loadUnpickedData()
|
// this.loadPickedData()
|
} else {
|
this.$message.error(response.message || '操作失败')
|
}
|
})
|
} catch (error) {
|
console.error('操作失败:', error)
|
this.$message.error('操作失败')
|
} finally {
|
this.executeLoading = false
|
}
|
},
|
|
handleDialogClose() {
|
if (!this.executeLoading) {
|
this.confirmDialogVisible = false
|
}
|
},
|
|
quickPick(row) {
|
this.scanForm.materialBarcode = row.materielCode
|
this.handleConfirmPick()
|
},
|
|
undoPick(row) {
|
try {
|
this.$confirm('确定要撤销这条拣选记录吗?', '提示', {
|
type: 'warning'
|
})
|
|
const response = this.http.post('/api/Outbound/UndoPicking', {
|
id: row.id
|
}).then(response => {
|
if (response.status) {
|
this.$message.success('撤销成功')
|
this.loadUnpickedData()
|
this.loadPickedData()
|
} else {
|
this.$message.error(response.message || '撤销失败')
|
}
|
})
|
} catch (error) {
|
if (error !== 'cancel') {
|
console.error('撤销失败:', error)
|
this.$message.error('撤销失败')
|
}
|
}
|
},
|
|
// handleUnpickedRowClick(row) {
|
// // 点击未拣选行时自动填充物料条码
|
// this.scanForm.materialBarcode = row.materielCode
|
// },
|
|
refreshUnpickedTable() {
|
if (this.scanForm.palletCode) {
|
this.loadPalletData()
|
}
|
},
|
|
refreshPickedTable() {
|
this.loadPickedData()
|
},
|
|
resetMaterialBarcode() {
|
this.scanForm.materialBarcode = ''
|
this.$nextTick(() => {
|
if (this.$refs.materialInput) {
|
this.$refs.materialInput.focus()
|
}
|
})
|
},
|
|
resetForm() {
|
this.scanForm.palletCode = ''
|
this.scanForm.materialBarcode = ''
|
this.unpickedData = []
|
this.$nextTick(() => {
|
if (this.$refs.palletInput) {
|
this.$refs.palletInput.focus()
|
}
|
})
|
},
|
|
getUserName() {
|
// 尝试从 Vuex store 获取用户名
|
if (this.$store && this.$store.state && this.$store.state.userInfo) {
|
return this.$store.state.userInfo.userName || this.$store.state.userInfo.username || '未登录用户'
|
}
|
|
// 尝试从 localStorage 获取
|
try {
|
const userInfo = localStorage.getItem('user')
|
if (userInfo) {
|
const user = JSON.parse(userInfo)
|
return user.userName || user.username || '未登录用户'
|
}
|
} catch (error) {
|
console.error('获取用户信息失败:', error)
|
}
|
|
return '未登录用户'
|
},
|
|
getStatusType(status) {
|
const statusMap = {
|
0: 'info', // 待处理
|
10: 'warning', // 进行中
|
20: 'primary', // 拣选中
|
30: 'success', // 已完成
|
40: 'danger' // 异常
|
}
|
return statusMap[status] || 'info'
|
}
|
}
|
}
|
</script>
|
|
<style scoped>
|
.picking-container {
|
padding: 20px;
|
background-color: #f5f5f5;
|
min-height: 100vh;
|
}
|
|
/* 订单信息卡片 */
|
.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;
|
}
|
|
.button-form-item {
|
margin-bottom: 0;
|
}
|
|
.button-form-item .el-form-item__content {
|
line-height: normal;
|
}
|
|
.action-buttons {
|
display: flex;
|
gap: 8px;
|
width: 100%;
|
height: 40px;
|
}
|
|
.action-buttons .el-button {
|
flex: 1;
|
height: 40px;
|
font-weight: bold;
|
border-radius: 6px;
|
margin: 0;
|
}
|
|
/* 表格区域 */
|
.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;
|
}
|
|
/* 响应式设计 */
|
@media (max-width: 1200px) {
|
.action-buttons .el-button {
|
font-size: 14px;
|
}
|
}
|
|
/* 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--warning {
|
background: linear-gradient(135deg, #E6A23C 0%, #d9971a 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-check,
|
.el-icon-delete,
|
.el-icon-refresh-left,
|
.el-icon-time,
|
.el-icon-circle-check {
|
font-size: 18px;
|
}
|
|
/* 描述列表样式 */
|
::v-deep .el-descriptions__label {
|
font-weight: bold;
|
color: #909399;
|
}
|
|
::v-deep .el-descriptions__content {
|
color: #606266;
|
}
|
</style>
|