<template>
|
<vol-box
|
v-model="groupPalletVisible"
|
:title="'组盘操作 - 单据号:' + currentDocNo"
|
:height="1000"
|
:width="1100"
|
:padding="20"
|
:modal="true"
|
|
@open="handleDialogOpen"
|
@close="handleDialogClose"
|
>
|
<div class="barcode-scanner-container">
|
|
<!-- 托盘信息显示 -->
|
<div class="tray-info" v-if="trayBarcode">
|
<i class="el-icon-s-management"></i> 当前托盘: {{ trayBarcode }}
|
<!-- <el-button
|
class="small-button"
|
type="text"
|
@click="clearTray"
|
style="float: right;"
|
>
|
清除托盘
|
</el-button> -->
|
</div>
|
|
<!-- 扫码或手动输入区域 -->
|
<div class="input-section">
|
<el-card shadow="hover">
|
<div slot="header">
|
<span><i class="el-icon-scanner"></i> </span>
|
<span class="scan-status">
|
<span class="scan-indicator"></span>扫码就绪
|
</span>
|
</div>
|
|
<!-- 托盘条码输入 -->
|
<div class="input-wrapper custom-input-group">
|
<div class="input-label">托盘条码</div>
|
<el-input
|
ref="trayInput"
|
v-model="trayBarcode"
|
placeholder="请扫描或输入托盘条码后按回车键"
|
clearable
|
@keyup.enter.native="handleTraySubmit"
|
@clear="handleTrayClear"
|
@input="handleTrayInput"
|
class="custom-input"
|
>
|
<template slot="prepend">
|
<span>托盘条码</span>
|
</template>
|
<template slot="append">
|
<el-button
|
@click="handleTraySubmit"
|
type="primary"
|
icon="el-icon-position"
|
>
|
确认
|
</el-button>
|
</template>
|
</el-input>
|
</div>
|
|
<!-- 物料条码输入 -->
|
<div class="input-wrapper custom-input-group">
|
<div class="input-label">物料条码</div>
|
<el-input
|
ref="barcodeInput"
|
v-model="barcode"
|
placeholder="请扫描或输入物料条码后按回车键"
|
clearable
|
:disabled="!trayBarcode"
|
@keyup.enter.native="handleBarcodeSubmit"
|
@clear="handleClear"
|
@input="handleBarcodeInput"
|
class="custom-input"
|
>
|
<template slot="prepend">
|
<span>物料条码</span>
|
</template>
|
<template slot="append">
|
<el-button
|
:loading="loading"
|
@click="handleBarcodeSubmit"
|
type="primary"
|
icon="el-icon-search"
|
:disabled="!trayBarcode"
|
>
|
{{ loading ? '查询中...' : '查询' }}
|
</el-button>
|
</template>
|
</el-input>
|
</div>
|
|
<div class="input-tips">
|
<p>提示:先输入托盘条码,然后输入物料条码</p>
|
|
</div>
|
|
</el-card>
|
</div>
|
|
<!-- 加载状态 -->
|
<div v-if="loading" class="loading">
|
<el-progress :percentage="100" status="success" :show-text="false" />
|
<p>正在查询物料信息...</p>
|
</div>
|
|
<!-- 错误提示 -->
|
<div v-if="error" class="error-message">
|
<el-alert
|
:title="error"
|
type="error"
|
show-icon
|
closable
|
@close="error = ''"
|
/>
|
</div>
|
|
<!-- 物料列表 -->
|
<div class="material-list">
|
<el-card shadow="hover">
|
<div slot="header">
|
<span><i class="el-icon-tickets"></i> 组盘数据</span>
|
<span class="list-actions">
|
<el-tag type="primary">共 {{ materials.length }} 条记录</el-tag>
|
<el-tag v-if="trayBarcode" type="success">托盘: {{ trayBarcode }}</el-tag>
|
<!-- <el-button
|
v-if="materials.length > 0"
|
class="small-button clear-all-btn"
|
type="danger"
|
icon="el-icon-delete"
|
@click="clearAllMaterials"
|
>
|
清空列表
|
</el-button> -->
|
<!-- <el-button
|
class="small-button clear-all-btn"
|
@click="debugMode = !debugMode"
|
>
|
{{ debugMode ? '隐藏调试' : '显示调试' }}
|
</el-button> -->
|
</span>
|
</div>
|
|
<div v-if="materials.length === 0" class="empty-state">
|
<i class="el-icon-document"></i>
|
<p v-if="!trayBarcode">请先输入托盘条码</p>
|
<p v-else>暂无物料数据,请扫描或输入物料条码</p>
|
</div>
|
|
<el-table
|
v-else
|
:data="materials"
|
stripe
|
style="width: 100%"
|
>
|
<el-table-column type="index" label="序号" width="60" align="center"></el-table-column>
|
<el-table-column prop="barcode" label="条码" min-width="140"></el-table-column>
|
<el-table-column prop="materielCode" label="物料编码" min-width="150"></el-table-column>
|
<el-table-column prop="batchNo" label="批次" min-width="150"></el-table-column>
|
<el-table-column prop="stockQuantity" label="数量" min-width="130"></el-table-column>
|
<el-table-column prop="unit" label="单位" width="80" align="center"></el-table-column>
|
<el-table-column prop="supplyCode" label="供应商" min-width="130"></el-table-column>
|
<el-table-column prop="warehouseCode" label="仓库" min-width="120"></el-table-column>
|
|
<!-- <el-table-column label="操作" width="100" align="center">
|
<template slot-scope="scope">
|
<el-button
|
v-if="scope"
|
class="small-button"
|
type="danger"
|
icon="el-icon-delete"
|
circle
|
@click="removeMaterial(scope.$index)"
|
></el-button>
|
</template>
|
</el-table-column> -->
|
</el-table>
|
</el-card>
|
</div>
|
</div>
|
|
<!-- <div slot="footer" class="dialog-footer">
|
<el-button @click="handleCancel">取消</el-button>
|
<el-button type="primary" @click="handleConfirm">确认</el-button>
|
</div> -->
|
</vol-box>
|
</template>
|
|
<script>
|
import http from '@/api/http.js';
|
import VolBox from '@/components/basic/VolBox.vue';
|
import VolForm from '@/components/basic/VolForm.vue';
|
import VolTable from '@/components/basic/VolTable.vue';
|
import { ElLoading, ElMessage,ElMessageBox } from 'element-plus';
|
import { ref, onMounted, onUnmounted } from 'vue'
|
|
export default {
|
name: 'BarcodeScanner',
|
components: { VolBox, VolForm, VolTable },
|
props: {
|
docNo: { type: String, required: true, default: '' },
|
visible: { type: Boolean, required: true, default: false }
|
},
|
|
data() {
|
return {
|
palletVisible: this.visible,
|
trayBarcode: '',
|
barcode: '',
|
materials: [],
|
loading: false,
|
error: '',
|
debugMode: false,
|
currentFocus: 'tray',
|
|
// 扫码枪相关变量
|
scanCode: '',
|
lastKeyTime: null,
|
isManualInput: false,
|
isScanning: false,
|
scanTimer: null,
|
manualInputTimer: null,
|
scanTarget: 'tray', // 当前扫码目标: tray 或 material
|
}
|
},
|
computed: {
|
groupPalletVisible: {
|
get() { return this.visible; },
|
set(newVal) { this.$emit('update:visible', newVal); }
|
},
|
currentDocNo() { return this.docNo; }
|
},
|
watch: {
|
visible(newVal, oldVal) {
|
this.palletVisible = newVal;
|
|
// 当从 false 变为 true 时,表示弹框打开
|
if (newVal === true && oldVal === false) {
|
console.log('弹框打开,重置数据');
|
this.resetData();
|
this.$nextTick(() => {
|
setTimeout(() => {
|
this.focusTrayInput();
|
}, 300);
|
});
|
}
|
|
// 当从 true 变为 false 时,表示弹框关闭
|
if (newVal === false && oldVal === true) {
|
console.log('弹框关闭,重置数据');
|
this.resetData();
|
}
|
},
|
palletVisible(newVal) {
|
this.$emit('update:visible', newVal);
|
},
|
docNo(newVal) {
|
if (newVal) {
|
this.palletForm = { palletCode: '', barcode: '' };
|
this.backData = [];
|
this.$refs.palletForm?.reset();
|
}
|
}
|
},
|
|
mounted() {
|
|
// 添加全局键盘监听
|
document.addEventListener('keypress', this.handleKeyPress);
|
|
// 使用setTimeout确保DOM完全渲染后再聚焦
|
setTimeout(() => {
|
this.focusTrayInput();
|
}, 300);
|
},
|
beforeDestroy() {
|
// 清理事件监听
|
document.removeEventListener('keypress', this.handleKeyPress);
|
this.clearAllTimers();
|
},
|
methods: {
|
// 重置所有数据
|
resetData() {
|
console.log('重置弹框数据');
|
this.trayBarcode = '';
|
this.barcode = '';
|
this.materials = [];
|
this.loading = false;
|
this.error = '';
|
this.scanCode = '';
|
this.lastKeyTime = null;
|
this.isManualInput = false;
|
this.isScanning = false;
|
this.currentFocus = 'tray';
|
this.scanTarget = 'tray';
|
this.clearAllTimers();
|
},
|
|
// 清除所有计时器
|
clearAllTimers() {
|
if (this.manualInputTimer) {
|
clearTimeout(this.manualInputTimer);
|
this.manualInputTimer = null;
|
}
|
if (this.scanTimer) {
|
clearTimeout(this.scanTimer);
|
this.scanTimer = null;
|
}
|
},
|
|
// 弹框打开时重置数据
|
handleDialogOpen() {
|
console.log('弹框打开,重置数据');
|
this.resetData();
|
// 使用setTimeout确保DOM完全渲染后再聚焦
|
this.$nextTick(() => {
|
setTimeout(() => {
|
this.focusTrayInput();
|
}, 300);
|
});
|
},
|
|
// 弹框关闭时重置数据
|
handleDialogClose() {
|
console.log('弹框关闭,重置数据');
|
this.resetData();
|
},
|
|
// 取消按钮
|
handleCancel() {
|
this.palletVisible = false;
|
},
|
|
// 确认按钮
|
handleConfirm() {
|
if (this.materials.length === 0) {
|
this.$message.warning('请至少添加一个物料');
|
return;
|
}
|
|
if (!this.trayBarcode) {
|
this.$message.warning('请输入托盘条码');
|
return;
|
}
|
|
const result = {
|
trayBarcode: this.trayBarcode,
|
materials: this.materials,
|
docNo: this.docNo
|
};
|
|
// 触发父组件的 back-success 事件
|
this.$emit('back-success', result);
|
this.palletVisible = false;
|
},
|
// 聚焦到托盘输入框
|
focusTrayInput() {
|
if (this.$refs.trayInput && this.$refs.trayInput.$el) {
|
const inputEl = this.$refs.trayInput.$el.querySelector('input');
|
if (inputEl) {
|
inputEl.focus();
|
this.currentFocus = 'tray';
|
this.scanTarget = 'tray';
|
}
|
}
|
},
|
|
// 聚焦到物料输入框
|
focusBarcodeInput() {
|
if (this.$refs.barcodeInput && this.$refs.barcodeInput.$el) {
|
const inputEl = this.$refs.barcodeInput.$el.querySelector('input');
|
if (inputEl) {
|
inputEl.focus();
|
this.currentFocus = 'material';
|
this.scanTarget = 'material';
|
}
|
}
|
},
|
|
// 处理托盘输入
|
handleTrayInput() {
|
// 标记为手动输入模式
|
this.isManualInput = true;
|
this.isScanning = false;
|
|
// 清除之前的计时器
|
if (this.manualInputTimer) {
|
clearTimeout(this.manualInputTimer);
|
}
|
|
// 设置计时器,如果一段时间内没有输入,则重置为扫码模式
|
this.manualInputTimer = setTimeout(() => {
|
this.isManualInput = false;
|
}, 1000);
|
},
|
|
// 处理物料输入
|
handleBarcodeInput() {
|
// 标记为手动输入模式
|
this.isManualInput = true;
|
this.isScanning = false;
|
|
// 清除之前的计时器
|
if (this.manualInputTimer) {
|
clearTimeout(this.manualInputTimer);
|
}
|
|
// 设置计时器,如果一段时间内没有输入,则重置为扫码模式
|
this.manualInputTimer = setTimeout(() => {
|
this.isManualInput = false;
|
}, 1000);
|
},
|
|
// 处理托盘条码提交
|
handleTraySubmit() {
|
const currentTrayBarcode = this.trayBarcode.trim();
|
|
if (!currentTrayBarcode) {
|
this.error = '请输入或扫描托盘条码';
|
return;
|
}
|
|
this.error = '';
|
|
// 设置托盘条码后,自动聚焦到物料输入框
|
this.focusBarcodeInput();
|
|
this.$message({
|
message: `托盘条码已设置: ${currentTrayBarcode}`,
|
type: 'success',
|
duration: 2000
|
});
|
},
|
|
// 清除托盘
|
clearTray() {
|
this.trayBarcode = '';
|
this.materials = [];
|
this.focusTrayInput();
|
this.$message({
|
message: '托盘条码已清除',
|
type: 'info',
|
duration: 2000
|
});
|
},
|
|
// 清空托盘输入
|
handleTrayClear() {
|
this.error = '';
|
},
|
|
// 清空输入
|
handleClear() {
|
this.error = '';
|
this.scanCode = '';
|
this.isManualInput = false;
|
this.isScanning = false;
|
},
|
|
// 处理物料条码提交
|
async handleBarcodeSubmit() {
|
const currentBarcode = this.barcode.trim();
|
|
if (!this.trayBarcode) {
|
this.error = '请先输入托盘条码';
|
this.focusTrayInput();
|
return;
|
}
|
|
if (!currentBarcode) {
|
this.error = '请输入或扫描物料条码';
|
return;
|
}
|
|
this.error = '';
|
this.loading = true;
|
|
try {
|
// 调用API查询物料信息
|
const materialData = await this.fetchMaterialData(currentBarcode);
|
if (!materialData || materialData.length === 0) {
|
|
|
return;
|
}
|
// 检查是否已存在相同物料编码的记录
|
const exists = this.materials.some(item =>
|
item.barcode === this.trayBarcode
|
);
|
console.log('API:',materialData)
|
if (exists) {
|
this.$message({
|
message: '该条码已存在当前托盘的列表中',
|
type: 'warning',
|
duration: 2000
|
});
|
} else {
|
|
materialData.forEach(item => {
|
|
// 如果不存在,添加新物料
|
this.materials.push({
|
...item,
|
trayCode: this.trayBarcode,
|
scanTime: this.formatTime(new Date())
|
});
|
});
|
|
|
|
|
this.$message({
|
message: `成功添加条码: ${currentBarcode}`,
|
type: 'success',
|
duration: 2000
|
});
|
|
|
// 清空物料输入框并保持聚焦
|
this.barcode = '';
|
this.scanCode = ''; // 清空扫码缓存
|
this.isScanning = false;
|
|
setTimeout(() => {
|
this.focusBarcodeInput();
|
}, 100);
|
}
|
} catch (err) {
|
this.error = err.message || '查询条码信息失败,请重试';
|
} finally {
|
this.loading = false;
|
}
|
},
|
|
// API请求 - 替换为实际的API调用
|
async fetchMaterialData(barcode) {
|
try {
|
const response = await http.post('/api/InboundOrder/BarcodeMaterielGroup',
|
{
|
palletCode: this.trayBarcode,
|
orderNo: this.docNo,
|
barcodes: barcode
|
}
|
);
|
|
|
let materialData;
|
|
if (typeof response.data === 'string') {
|
|
try {
|
materialData = JSON.parse(response.data);
|
} catch (e) {
|
|
}
|
} else {
|
// 如果返回的是JSON对象,直接使用
|
materialData = response.data;
|
}
|
if(!response.status){
|
this.error = response.message || '查询条码信息失败,请重试';
|
}
|
// 确保返回的数据包含所有必需的字段
|
return materialData;
|
|
} catch (error) {
|
console.error('API调用失败:', error);
|
|
|
}
|
},
|
|
// 处理扫码枪输入
|
handleKeyPress(event) {
|
// 如果是手动输入模式,不处理扫码枪逻辑
|
if (this.isManualInput) {
|
return;
|
}
|
|
const key = event.key;
|
const currentTime = new Date().getTime();
|
|
// 忽略直接按下的回车键(由handleBarcodeSubmit处理)
|
if (key === 'Enter') {
|
if (this.scanCode.length > 0) {
|
// 阻止默认回车行为,避免表单提交
|
event.preventDefault();
|
|
// 扫码完成,自动触发查询
|
this.isScanning = false;
|
|
// 根据当前扫码目标设置相应的输入框值
|
if (this.scanTarget === 'tray') {
|
this.trayBarcode = this.scanCode;
|
this.handleTraySubmit();
|
} else if (this.scanTarget === 'material') {
|
this.barcode = this.scanCode;
|
this.handleBarcodeSubmit();
|
}
|
}
|
this.scanCode = '';
|
this.lastKeyTime = null;
|
return;
|
}
|
|
// 构建扫码内容(快速连续输入视为扫码)
|
if (this.lastKeyTime && currentTime - this.lastKeyTime < 50) {
|
this.scanCode += key;
|
this.isScanning = true;
|
} else {
|
this.scanCode = key;
|
this.isScanning = true;
|
}
|
|
// 设置计时器,如果一段时间内没有输入,则重置扫描状态
|
if (this.scanTimer) {
|
clearTimeout(this.scanTimer);
|
}
|
this.scanTimer = setTimeout(() => {
|
this.isScanning = false;
|
}, 100);
|
|
this.lastKeyTime = currentTime;
|
},
|
|
// 删除物料
|
removeMaterial(index) {
|
this.$confirm('确定要删除这条物料记录吗?', '提示', {
|
confirmButtonText: '确定',
|
cancelButtonText: '取消',
|
type: 'warning'
|
}).then(() => {
|
this.materials.splice(index, 1);
|
this.$message({
|
type: 'success',
|
message: '删除成功!'
|
});
|
}).catch(() => {
|
// 取消删除
|
});
|
},
|
|
// 清空所有物料
|
clearAllMaterials() {
|
if (this.materials.length === 0) return;
|
|
this.$confirm('确定要清空所有物料记录吗?', '提示', {
|
confirmButtonText: '确定',
|
cancelButtonText: '取消',
|
type: 'warning'
|
}).then(() => {
|
this.materials = [];
|
this.$message({
|
type: 'success',
|
message: '已清空所有记录!'
|
});
|
}).catch(() => {
|
// 取消清空
|
});
|
},
|
|
// 格式化时间
|
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}`;
|
}
|
}
|
}
|
</script>
|
|
<style scoped>
|
.barcode-scanner-container {
|
max-width: 1200px;
|
margin: 0 auto;
|
padding: 20px;
|
}
|
.page-title {
|
text-align: center;
|
margin-bottom: 30px;
|
}
|
.input-section {
|
margin-bottom: 30px;
|
}
|
.scan-status {
|
float: right;
|
font-size: 12px;
|
color: #67C23A;
|
}
|
.scan-indicator {
|
display: inline-block;
|
width: 10px;
|
height: 10px;
|
border-radius: 50%;
|
background-color: #67C23A;
|
margin-right: 5px;
|
animation: pulse 1.5s infinite;
|
}
|
@keyframes pulse {
|
0% { opacity: 1; }
|
50% { opacity: 0.4; }
|
100% { opacity: 1; }
|
}
|
.input-wrapper {
|
position: relative;
|
margin-bottom: 15px;
|
}
|
.input-tips {
|
margin-top: 10px;
|
font-size: 12px;
|
color: #909399;
|
}
|
.loading {
|
text-align: center;
|
margin: 20px 0;
|
}
|
.loading p {
|
margin-top: 10px;
|
color: #409EFF;
|
}
|
.error-message {
|
margin-bottom: 20px;
|
}
|
.material-list {
|
margin-top: 30px;
|
}
|
.list-actions {
|
float: right;
|
}
|
.clear-all-btn {
|
margin-left: 10px;
|
}
|
.empty-state {
|
text-align: center;
|
padding: 40px 0;
|
color: #909399;
|
}
|
.empty-state i {
|
font-size: 48px;
|
margin-bottom: 10px;
|
}
|
.material-code {
|
font-family: 'Courier New', monospace;
|
font-weight: bold;
|
color: #409EFF;
|
}
|
.tray-info {
|
background: #f0f9ff;
|
padding: 10px 15px;
|
border-radius: 4px;
|
margin-bottom: 15px;
|
border-left: 4px solid #409EFF;
|
}
|
.debug-info {
|
background: #f5f7fa;
|
padding: 10px;
|
border-radius: 4px;
|
margin-top: 10px;
|
font-size: 12px;
|
color: #909399;
|
}
|
.small-button {
|
padding: 7px 9px;
|
font-size: 12px;
|
}
|
.custom-input-group {
|
display: flex;
|
align-items: center;
|
width: 100%;
|
margin: 20px 0;
|
border: 1px solid #DCDFE6;
|
border-radius: 4px;
|
overflow: hidden;
|
background: #fff;
|
}
|
|
.input-label {
|
padding: 0 15px;
|
background: #F5F7FA;
|
border-right: 1px solid #DCDFE6;
|
color: #606266;
|
font-size: 14px;
|
white-space: nowrap;
|
height: 40px;
|
line-height: 40px;
|
flex-shrink: 0;
|
}
|
|
.input-container {
|
display: flex;
|
flex: 1;
|
align-items: center;
|
}
|
|
.custom-input {
|
flex: 1;
|
}
|
|
.custom-input ::v-deep .el-input__inner {
|
border: none;
|
border-radius: 0;
|
height: 40px;
|
line-height: 40px;
|
}
|
|
|
</style>
|