<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>1. 输入/扫描托盘码 → 2. 回车触发系统获取订单号并加载数据</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="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 class="picking-stats" v-if="scanForm.palletCode && unpickedData.length > 0">
|
<el-divider content-position="left">
|
<span style="color: #409EFF; font-size: 14px;">
|
<i class="el-icon-data-analysis"></i> 托盘分拣统计
|
</span>
|
</el-divider>
|
<div class="stats-container">
|
<div class="stat-item">
|
<el-tag type="primary" size="medium" effect="dark">
|
<i class="el-icon-s-order"></i>
|
分拣总数:<b>{{ calculateTotalAssignQuantity() }}</b>
|
</el-tag>
|
</div>
|
<div class="stat-item">
|
<el-tag type="success" size="medium" effect="dark">
|
<i class="el-icon-circle-check"></i>
|
已分拣:<b>{{ calculateTotalSortedQuantity() }}</b>
|
</el-tag>
|
</div>
|
<div class="stat-item">
|
<el-tag type="warning" size="medium" effect="dark">
|
<i class="el-icon-time"></i>
|
未分拣:<b>{{ calculateTotalUnsortedQuantity() }}</b>
|
</el-tag>
|
</div>
|
<div class="stat-item">
|
<el-tag :type="hasWholeOut() ? 'success' : 'warning'" size="medium" effect="dark">
|
<i class="el-icon-box"></i>
|
是否整出:{{ hasWholeOut() ? '是' : '否' }}
|
</el-tag>
|
</div>
|
</div>
|
</div>
|
</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>
|
|
<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>
|
|
<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>
|
|
<print-view ref="printView" @parentcall="parentcall"></print-view>
|
|
<!-- 确认对话框 -->
|
<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>
|
|
<!-- 整出确认对话框 -->
|
<el-dialog v-model="wholeOutDialogVisible" title="整出操作确认" width="500px" :before-close="handleWholeOutDialogClose"
|
custom-class="whole-out-dialog" style="margin-right: 0px;">
|
<div class="whole-out-content" v-if="wholeOutInfo">
|
<!-- 警告提示 -->
|
<el-alert title="该托盘包含需要整出的物料" type="warning" :closable="false" show-icon class="whole-out-alert">
|
<template #default>
|
<div>整出操作将一次性拣选该物料的所有库存,请确认信息无误后执行</div>
|
</template>
|
</el-alert>
|
|
<!-- 托盘信息 -->
|
<div class="info-section">
|
<h4 class="section-title">
|
<i class="el-icon-box"></i>
|
托盘信息
|
</h4>
|
<div class="info-grid">
|
<div class="info-item">
|
<label>托盘码:</label>
|
<span class="info-value">{{ wholeOutInfo.palletCode }}</span>
|
</div>
|
<div class="info-item">
|
<label>库位:</label>
|
<span class="info-value">{{ wholeOutInfo.locationCode || '未指定' }}</span>
|
</div>
|
</div>
|
</div>
|
|
<!-- 整出物料详情 -->
|
<div class="info-section">
|
<h4 class="section-title">
|
<i class="el-icon-s-grid"></i>
|
整出物料详情
|
</h4>
|
<div class="info-grid">
|
<div class="info-item">
|
<label>物料编码:</label>
|
<span class="info-value">{{ wholeOutInfo.materielCode }}</span>
|
</div>
|
<div class="info-item">
|
<label>物料名称:</label>
|
<span class="info-value">{{ wholeOutInfo.materielName }}</span>
|
</div>
|
<div class="info-item">
|
<label>批次号:</label>
|
<span class="info-value">{{ wholeOutInfo.batchNo }}</span>
|
</div>
|
<div class="info-item">
|
<label>整出数量:</label>
|
<span class="info-value highlight">{{ wholeOutInfo.assignQuantity }} {{ wholeOutInfo.unit }}</span>
|
</div>
|
<div class="info-item">
|
<label>当前库存:</label>
|
<span class="info-value">{{ wholeOutInfo.currentStock || wholeOutInfo.originalQuantity }} {{ wholeOutInfo.unit }}</span>
|
</div>
|
</div>
|
</div>
|
|
<!-- 操作提示 -->
|
<div class="operation-tip">
|
<i class="el-icon-info"></i>
|
<span>确认执行整出操作吗?此操作将一次性拣选该物料的所有库存。</span>
|
</div>
|
</div>
|
<template #footer>
|
<span class="dialog-footer">
|
<el-button @click="wholeOutDialogVisible = false" size="medium">取消</el-button>
|
<el-button type="warning" @click="executeWholeOut" :loading="executeLoading" size="medium">
|
<i class="el-icon-check"></i>
|
确认整出
|
</el-button>
|
</span>
|
</template>
|
</el-dialog>
|
</div>
|
</template>
|
|
<script>
|
import printView from "@/extension/outbound/extend/printView.vue"
|
import { stationManager, STATION_STORAGE_KEY } from "@/../src/uitils/stationManager";
|
import { ElLoading } from 'element-plus'
|
|
// 导入音频文件(适配src/assets目录,webpack自动处理)
|
const successAudioSrc = require('@/assets/audio/success.mp3');
|
const errorAudioSrc = require('@/assets/audio/error.mp3');
|
|
export default {
|
components: { printView },
|
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,
|
matMixed: true,
|
wholeOutDialogVisible: false,
|
wholeOutInfo: null,
|
globalLoading: false,
|
loadingInstance: null,
|
// 音频实例(缓存,避免重复创建)
|
successAudio: null,
|
errorAudio: null,
|
selectedPalletCode: ''
|
}
|
},
|
computed: {
|
canConfirm() {
|
return this.scanForm.palletCode && this.scanForm.materialBarcode
|
}
|
},
|
mounted() {
|
this.initPage()
|
// 初始化音频实例(懒加载,仅创建一次)
|
this.initAudioInstance()
|
},
|
beforeDestroy() {
|
// 销毁音频实例,释放资源
|
this.successAudio = null
|
this.errorAudio = null
|
// 【已清理】移除即时触发相关的防抖定时器清除代码
|
},
|
methods: {
|
// 初始化音频实例(核心:适配src/assets路径,缓存实例)
|
initAudioInstance() {
|
// 成功音频实例
|
if (!this.successAudio) {
|
this.successAudio = new Audio(successAudioSrc)
|
this.successAudio.onerror = (err) => {
|
console.error('【成功音频】加载失败', err)
|
}
|
}
|
if (!this.errorAudio) {
|
this.errorAudio = new Audio(errorAudioSrc)
|
this.errorAudio.onerror = (err) => {
|
console.error('【错误音频】加载失败', err)
|
}
|
}
|
},
|
// 播放成功音频
|
playSuccessAudio() {
|
try {
|
// 重置播放进度(避免重复播放时音频未结束)
|
this.successAudio.currentTime = 0
|
// 播放(兼容浏览器自动播放策略限制)
|
this.successAudio.play().catch((err) => {
|
console.warn('成功音频播放失败(浏览器自动播放策略限制)', err)
|
})
|
} catch (err) {
|
console.error('播放成功音频异常', err)
|
}
|
},
|
playErrorAudio() {
|
try {
|
this.errorAudio.currentTime = 0
|
this.errorAudio.play().catch((err) => {
|
console.warn('错误音频播放失败(浏览器自动播放策略限制)', err)
|
})
|
} catch (err) {
|
console.error('播放错误音频异常', err)
|
}
|
},
|
initPage() {
|
// 从路由参数获取订单号
|
this.orderNo = this.$route.query.orderNo || '';
|
|
// 支持通过托盘号获取订单号,不强制返回上一页
|
if (this.orderNo) {
|
this.loadOrderInfo();
|
} else {
|
this.$message.info('请扫描/输入托盘码获取关联订单');
|
}
|
|
// 自动聚焦到托盘码输入框
|
this.$nextTick(() => {
|
if (this.$refs.palletInput) {
|
this.$refs.palletInput.focus();
|
}
|
});
|
},
|
async loadOrderInfo() {
|
if (!this.orderNo) return;
|
|
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(`获取订单信息异常:${error.message || '网络错误'}`);
|
console.error('【加载订单信息失败】', error);
|
} finally {
|
this.hideFullScreenLoading();
|
}
|
},
|
async loadPalletData() {
|
if (!this.scanForm.palletCode || !this.orderNo) {
|
this.unpickedData = [];
|
return;
|
}
|
|
try {
|
this.showFullScreenLoading();
|
await Promise.all([
|
this.loadUnpickedData(),
|
this.loadPickedData()
|
]);
|
} catch (error) {
|
this.unpickedData = [];
|
console.error('【加载托盘数据失败】', error);
|
} finally {
|
this.hideFullScreenLoading();
|
}
|
},
|
// 核心逻辑:自动提取CK订单号并赋值,隐藏指定提示
|
loadUnpickedData() {
|
return new Promise((resolve, reject) => {
|
// 先清空之前的提示,避免重复提示
|
this.$message.closeAll();
|
|
this.http.post(`/api/Outbound/QueryPickingTasks?orderNo=${this.orderNo}&palletCode=${this.scanForm.palletCode}`, {}).then(response => {
|
if (response.status) {
|
if (response.data.outStockLockInfos && response.data.outStockLockInfos.length > 0) {
|
this.unpickedData = response.data.outStockLockInfos;
|
this.matMixed = response.data.isMatMixed;
|
this.orderOver = response.data.orderOver;
|
this.calculateUnpickedStats();
|
|
this.$nextTick(() => {
|
if (this.hasWholeOut()) {
|
this.showWholeOutConfirm();
|
}
|
});
|
|
this.$nextTick(() => {
|
if (this.$refs.materialInput) {
|
this.$refs.materialInput.focus();
|
}
|
});
|
} else {
|
this.unpickedData = [];
|
this.calculateUnpickedStats();
|
|
this.$message.success({
|
message: `托盘【${this.scanForm.palletCode}】已拣选完成,暂无拣选记录`,
|
duration: 3000,
|
showClose: true
|
});
|
|
this.$nextTick(() => {
|
if (this.$refs.palletInput) {
|
this.$refs.palletInput.focus();
|
this.$refs.palletInput.$el.querySelector('input').select();
|
}
|
// 表格高亮选中当前托盘
|
if (this.$refs.unpickedTable) {
|
const targetRow = this.unpickedData.find(item => item.palletCode === this.scanForm.palletCode);
|
if (targetRow) this.$refs.unpickedTable.setCurrentRow(targetRow);
|
}
|
});
|
|
this.scanForm.materialBarcode = '';
|
this.$nextTick(() => {
|
if (this.$refs.palletInput) {
|
this.$refs.palletInput.focus();
|
}
|
});
|
}
|
resolve();
|
} else {
|
const errorMsg = response.message || `获取托盘【${this.scanForm.palletCode}】拣选数据失败`;
|
const targetOrderNo = this.extractCKOrderNo(errorMsg);
|
|
if (targetOrderNo) {
|
// 自动赋值,不显示错误提示,闭环流程
|
this.orderNo = targetOrderNo;
|
this.$forceUpdate();
|
this.loadOrderInfo().then(() => {
|
this.loadPalletData();
|
});
|
resolve("自动赋值新订单号,重新请求数据");
|
} else {
|
this.$message.error({
|
message: errorMsg,
|
duration: 5000,
|
showClose: true
|
});
|
|
this.scanForm.palletCode = '';
|
this.$nextTick(() => {
|
if (this.$refs.palletInput) {
|
this.$refs.palletInput.focus();
|
}
|
});
|
reject(errorMsg);
|
}
|
|
this.unpickedData = [];
|
this.calculateUnpickedStats();
|
}
|
}).catch(error => {
|
this.unpickedData = [];
|
this.calculateUnpickedStats();
|
|
this.$message.error({
|
message: `获取托盘【${this.scanForm.palletCode}】拣选数据异常:${error.message || '网络错误,请重试'}`,
|
duration: 5000,
|
showClose: true
|
});
|
console.error("【QueryPickingTasks 网络异常】", error);
|
reject(error);
|
})
|
})
|
},
|
loadPickedData() {
|
return new Promise((resolve, reject) => {
|
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.pickedData = [];
|
}
|
resolve();
|
} else {
|
const errorMsg = response.message || '获取托盘已拣选数据失败';
|
const targetOrderNo = this.extractCKOrderNo(errorMsg);
|
|
if (targetOrderNo) {
|
this.orderNo = targetOrderNo;
|
this.$forceUpdate();
|
resolve("自动赋值新订单号");
|
} else {
|
reject(errorMsg);
|
}
|
this.pickedData = [];
|
}
|
}).catch(error => {
|
console.error("【QueryPickedList 网络异常】", error);
|
reject(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;
|
},
|
// 工具方法 - 提取CK开头订单号
|
extractCKOrderNo(message) {
|
if (!message) return null;
|
const ckOrderReg = /CK\d+/g;
|
const matchResult = message.match(ckOrderReg);
|
return matchResult && matchResult.length > 0 ? matchResult[0] : null;
|
},
|
// 核心:调用获取订单号接口(完整路径,回车触发)
|
async getOrderNoByPallet(palletCode) {
|
if (!palletCode) {
|
this.$message.warning('托盘号不能为空');
|
return null;
|
}
|
|
try {
|
this.showFullScreenLoading();
|
const response = await this.http.get(`/api/OutboundOrder/GetOrderNoByPalletCode?palletCode=${palletCode}`);
|
|
if (response.status && response.data) {
|
const validOrderNo = response.data;
|
this.playSuccessAudio();
|
this.$message.success(`成功获取订单号:${validOrderNo}`);
|
return validOrderNo;
|
} else {
|
this.playErrorAudio();
|
const errorMsg = response.message || '该托盘号未关联任何订单';
|
this.$message.error(errorMsg);
|
return null;
|
}
|
} catch (error) {
|
this.playErrorAudio();
|
const errorMsg = `接口调用异常:${error.message || '网络错误/接口不存在'}`;
|
this.$message.error(errorMsg);
|
console.error("【接口调用失败】", error);
|
return null;
|
} finally {
|
this.hideFullScreenLoading();
|
}
|
},
|
// 回车触发的核心方法,无即时触发,避免频繁调用接口
|
async handlePalletScan(flag = true) {
|
const palletCode = this.scanForm.palletCode.trim();
|
if (!palletCode) {
|
this.$message.warning('请输入有效的托盘号');
|
return;
|
}
|
|
// 调用订单号接口,仅回车触发一次
|
const orderNoFromApi = await this.getOrderNoByPallet(palletCode);
|
|
if (orderNoFromApi) {
|
this.orderNo = orderNoFromApi;
|
this.$forceUpdate();
|
}
|
|
if (this.orderNo) {
|
await this.loadOrderInfo();
|
this.loadPalletData(flag);
|
} else {
|
this.loadPalletData(flag);
|
}
|
},
|
handleMaterialScan() {
|
if (!this.scanForm.palletCode) {
|
this.$message.warning('请先扫描/输入托盘码');
|
this.$refs.palletInput.focus();
|
return;
|
}
|
|
if (!this.scanForm.materialBarcode) {
|
this.$message.warning('请扫描物料条码');
|
return;
|
}
|
|
this.handleConfirmPick();
|
},
|
async handleConfirmPick() {
|
if (!this.scanForm.palletCode || !this.scanForm.materialBarcode) {
|
this.$message.warning('请先扫描托盘码和物料条码');
|
return;
|
}
|
|
this.confirmLoading = true;
|
this.showFullScreenLoading();
|
|
try {
|
const response = await this.http.post('/api/Outbound/CompleteOutboundWithBarcode', {
|
orderNo: this.orderNo,
|
palletCode: this.scanForm.palletCode,
|
barcode: this.scanForm.materialBarcode,
|
operator: this.getUserName()
|
});
|
if (response.status) {
|
if (response.data.scannedDetail.isUnpacked && response.data.scannedDetail.materialCodes.length > 0) {
|
this.$refs.printView.open(response.data.scannedDetail.materialCodes);
|
}
|
this.$message.success('拣选确认成功');
|
this.playSuccessAudio();
|
this.resetMaterialBarcode();
|
await this.loadPalletData(false);
|
} else {
|
this.$message.error(response.message || '拣选确认失败');
|
this.playErrorAudio();
|
}
|
} catch (error) {
|
this.$message.error('拣选确认失败');
|
this.playErrorAudio();
|
} finally {
|
this.confirmLoading = false;
|
this.hideFullScreenLoading();
|
}
|
},
|
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;
|
},
|
async executeConfirm() {
|
this.executeLoading = true;
|
this.showFullScreenLoading();
|
|
try {
|
let apiUrl = '';
|
let params = {
|
orderNo: this.orderNo,
|
palletCode: this.scanForm.palletCode,
|
station: stationManager.getStation()
|
};
|
if (this.currentAction === 'emptyBox') {
|
apiUrl = '/api/Outbound/EmptyBox';
|
} else if (this.currentAction === 'returnToWarehouse') {
|
apiUrl = '/api/Outbound/ReturnToWarehouse';
|
}
|
|
const response = await this.http.post(apiUrl, params);
|
|
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.hideFullScreenLoading();
|
}
|
},
|
handleDialogClose() {
|
if (!this.executeLoading) {
|
this.confirmDialogVisible = false;
|
}
|
},
|
showWholeOutConfirm() {
|
const wholeOutItem = this.unpickedData.find(item => item.assignQuantity === item.originalQuantity);
|
if (wholeOutItem) {
|
this.wholeOutInfo = {
|
palletCode: this.scanForm.palletCode,
|
locationCode: wholeOutItem.locationCode,
|
materielCode: wholeOutItem.materielCode,
|
materielName: wholeOutItem.materielName,
|
batchNo: wholeOutItem.batchNo,
|
assignQuantity: wholeOutItem.assignQuantity,
|
currentStock: wholeOutItem.currentStock,
|
originalQuantity: wholeOutItem.originalQuantity,
|
unit: wholeOutItem.unit
|
};
|
this.wholeOutDialogVisible = true;
|
}
|
},
|
handleWholeOutDialogClose() {
|
if (!this.executeLoading) {
|
this.wholeOutDialogVisible = false;
|
this.wholeOutInfo = null;
|
}
|
},
|
async executeWholeOut() {
|
if (!this.wholeOutInfo) {
|
this.$message.error('整出信息无效');
|
return;
|
}
|
|
this.executeLoading = true;
|
this.showFullScreenLoading();
|
|
try {
|
const response = await this.http.post('/api/Outbound/CompleteOutboundWithPallet', {
|
orderNo: this.orderNo,
|
palletCode: this.scanForm.palletCode,
|
operator: this.getUserName()
|
});
|
if (response.status) {
|
this.$message.success('整出操作成功');
|
this.wholeOutDialogVisible = false;
|
this.wholeOutInfo = null;
|
await this.loadPalletData();
|
} else {
|
this.$message.error(response.message || '整出操作失败');
|
}
|
} catch (error) {
|
this.$message.error('整出操作失败');
|
} finally {
|
this.executeLoading = false;
|
this.hideFullScreenLoading();
|
}
|
},
|
quickPick(row) {
|
this.scanForm.materialBarcode = row.materielCode;
|
this.handleConfirmPick();
|
},
|
undoPick(row) {
|
try {
|
this.$confirm('确定要撤销这条拣选记录吗?', '提示', {
|
type: 'warning'
|
}).then(() => {
|
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(() => {});
|
} catch (error) {
|
if (error !== 'cancel') {
|
this.$message.error('撤销失败');
|
}
|
}
|
},
|
refreshUnpickedTable() {
|
if (this.scanForm.palletCode) {
|
this.loadPalletData();
|
}
|
},
|
refreshPickedTable() {
|
this.loadPickedData().catch(error => {
|
console.error('刷新已拣选列表失败:', error);
|
});
|
},
|
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() {
|
if (this.$store && this.$store.state && this.$store.state.userInfo) {
|
return this.$store.state.userInfo.userName || this.$store.state.userInfo.username || '未登录用户';
|
}
|
|
try {
|
const userInfo = localStorage.getItem('user');
|
if (userInfo) {
|
const user = JSON.parse(userInfo);
|
return user.userName || user.username || '未登录用户';
|
}
|
} catch (error) {}
|
|
return '未登录用户';
|
},
|
calculateTotalAssignQuantity() {
|
return this.unpickedData.reduce((sum, item) => {
|
return sum + (item.assignQuantity || 0);
|
}, 0);
|
},
|
calculateTotalSortedQuantity() {
|
return this.unpickedData.reduce((sum, item) => {
|
return sum + (item.sortedQuantity || 0);
|
}, 0);
|
},
|
calculateTotalUnsortedQuantity() {
|
return this.unpickedData.reduce((sum, item) => {
|
const assignQty = item.assignQuantity || 0;
|
const sortedQty = item.sortedQuantity || 0;
|
return sum + Math.max(0, assignQty - sortedQty);
|
}, 0);
|
},
|
hasWholeOut() {
|
return this.unpickedData.some(item => item.assignQuantity === item.originalQuantity) && !this.matMixed && !this.orderOver;
|
},
|
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>
|
.el-dialog__header {
|
margin: 0;
|
}
|
</style>
|
|
<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;
|
flex-direction: column;
|
align-items: flex-start;
|
}
|
|
.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;
|
}
|
}
|
|
::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;
|
}
|
|
::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;
|
}
|
|
.picking-stats {
|
margin-top: 20px;
|
padding: 0 10px;
|
}
|
|
.stats-container {
|
display: flex;
|
flex-wrap: wrap;
|
gap: 12px;
|
align-items: center;
|
}
|
|
.stat-item {
|
display: inline-flex;
|
align-items: center;
|
}
|
|
.stat-item .el-tag {
|
display: flex;
|
align-items: center;
|
padding: 6px 12px;
|
font-size: 13px;
|
border-radius: 20px;
|
}
|
|
.stat-item .el-tag i {
|
margin-right: 6px;
|
font-size: 14px;
|
}
|
|
.stat-item b {
|
margin-left: 4px;
|
font-size: 14px;
|
}
|
|
::v-deep .el-divider__text {
|
background-color: #f5f5f5;
|
padding: 0 20px;
|
}
|
|
::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;
|
}
|
|
::v-deep .el-loading-mask {
|
background-color: rgba(0, 0, 0, 0.7) !important;
|
z-index: 9999 !important;
|
}
|
|
::v-deep .el-loading-spinner {
|
z-index: 10000 !important;
|
}
|
|
::v-deep .el-loading-text {
|
color: #ffffff !important;
|
font-weight: bold !important;
|
}
|
|
::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;
|
}
|
|
::v-deep .el-dialog {
|
z-index: 2000 !important;
|
}
|
|
::v-deep .el-dialog__wrapper {
|
z-index: 2000 !important;
|
}
|
</style>
|