<template>
|
<vol-box
|
v-model="show"
|
:title="'组盘操作 - 单据号:' + orderNo"
|
:height="1000"
|
:width="1100"
|
:padding="20"
|
:modal="true"
|
>
|
<div class="barcode-scanner-container">
|
<!-- 仓库选择 - 紧凑布局 -->
|
<div class="location-section compact">
|
<el-form
|
:model="form"
|
:rules="rules"
|
ref="locationForm"
|
class="compact-form"
|
>
|
<el-form-item
|
label="入库仓库"
|
prop="warehouseType"
|
class="location-select compact-item"
|
>
|
<el-select
|
v-model="form.warehouseType"
|
placeholder="请选择仓库"
|
clearable
|
filterable
|
@change="handleWarehouseChange"
|
style="width: 100%"
|
:loading="warehouseLoading"
|
size="medium"
|
>
|
<el-option
|
v-for="item in warehouseTypes"
|
:key="item.warehouseType"
|
:label="item.warehouseTypeDesc"
|
:value="item.warehouseType"
|
/>
|
</el-select>
|
</el-form-item>
|
</el-form>
|
</div>
|
|
<!-- 仓库区域选择 - 紧凑布局 -->
|
<div class="location-section compact">
|
<el-form
|
:model="form"
|
:rules="rules"
|
ref="locationForm"
|
class="compact-form"
|
>
|
<el-form-item
|
label="仓库区域"
|
prop="locationType"
|
class="location-select compact-item"
|
>
|
<el-select
|
v-model="form.locationType"
|
placeholder="自动获取仓库区域"
|
clearable
|
filterable
|
@change="handleLocationChange"
|
style="width: 100%"
|
:loading="locationLoading"
|
size="medium"
|
:disabled="true"
|
>
|
<el-option
|
v-for="item in locationTypes"
|
:key="item.locationType"
|
:label="item.locationTypeDesc"
|
:value="item.locationType"
|
/>
|
</el-select>
|
</el-form-item>
|
</el-form>
|
</div>
|
|
<!-- 托盘信息显示 - 紧凑布局 -->
|
<div class="tray-info compact" v-if="trayBarcode">
|
<i class="el-icon-s-management"></i> 当前料箱: {{ trayBarcode }}
|
<span class="location-info" v-if="form.warehouseType">
|
| 仓库: {{ currentWarehouseName }}
|
</span>
|
<span class="location-info" v-if="form.locationType">
|
| 仓库区域: {{ currentLocationDesc }}
|
</span>
|
</div>
|
|
<!-- 扫码区域 - 紧凑布局 -->
|
<div class="input-section compact">
|
<el-card shadow="hover" class="compact-card">
|
<div slot="header" class="compact-header">
|
<span><i class="el-icon-scanner"></i> 扫码区域</span>
|
<span class="scan-status">
|
<span class="scan-indicator"></span>
|
{{
|
form.locationType && form.warehouseType
|
? "扫码就绪"
|
: "请先选择仓库和仓库区域"
|
}}
|
</span>
|
</div>
|
|
<!-- 托盘条码输入 -->
|
<div class="input-wrapper custom-input-group compact-input">
|
<div class="input-label">托盘条码</div>
|
<el-input
|
ref="trayInput"
|
v-model="trayBarcode"
|
placeholder="请扫描或输入料箱码后按回车键"
|
clearable
|
:disabled="!form.locationType || !form.warehouseType"
|
@keyup.enter.native="handleTraySubmit"
|
@clear="handleTrayClear"
|
@input="handleTrayInput"
|
class="custom-input"
|
size="medium"
|
>
|
<template slot="append">
|
<el-button
|
@click="handleTraySubmit"
|
type="primary"
|
icon="el-icon-position"
|
:disabled="
|
!form.locationType || !trayBarcode || !form.warehouseType
|
"
|
size="medium"
|
>
|
确认
|
</el-button>
|
</template>
|
</el-input>
|
</div>
|
|
<!-- 物料条码输入 -->
|
<div class="input-wrapper custom-input-group compact-input">
|
<div class="input-label">物料条码</div>
|
<el-input
|
ref="barcodeInput"
|
v-model="barcode"
|
placeholder="请扫描或输入物料条码后按回车键"
|
clearable
|
:disabled="
|
!form.locationType || !trayBarcode || !form.warehouseType
|
"
|
@keyup.enter.native="debounceHandleBarcodeSubmit"
|
@clear="handleClear"
|
@input="handleBarcodeInput"
|
class="custom-input"
|
size="medium"
|
>
|
<template slot="append">
|
<el-button
|
:loading="loading"
|
@click="debounceHandleBarcodeSubmit"
|
type="primary"
|
icon="el-icon-search"
|
:disabled="
|
!form.locationType ||
|
!trayBarcode ||
|
!barcode ||
|
!form.warehouseType
|
"
|
size="medium"
|
>
|
{{ loading ? "查询中..." : "查询" }}
|
</el-button>
|
</template>
|
</el-input>
|
</div>
|
|
<div class="input-tips compact-tips">
|
<p>提示:请先选择仓库 → 选择仓库区域 → 输入料箱码 → 输入物料条码</p>
|
<p v-if="!form.warehouseType" class="warning-text">
|
⚠️ 请先选择仓库
|
</p>
|
<p
|
v-if="form.warehouseType && !form.locationType"
|
class="warning-text"
|
>
|
⚠️ 请先选择仓库区域
|
</p>
|
<p
|
v-if="form.warehouseType && form.locationType && !trayBarcode"
|
class="warning-text"
|
>
|
⚠️ 请先输入料箱码
|
</p>
|
</div>
|
</el-card>
|
</div>
|
|
<!-- 加载状态 -->
|
<div v-if="loading" class="loading compact">
|
<el-progress :percentage="100" status="success" :show-text="false" />
|
<p>正在查询物料信息...</p>
|
</div>
|
|
<!-- 错误提示 -->
|
<div v-if="error" class="error-message compact">
|
<el-alert
|
:title="error"
|
type="error"
|
show-icon
|
closable
|
@close="error = ''"
|
/>
|
</div>
|
|
<!-- 未组盘列表 -->
|
<div class="unpallet-section compact">
|
<el-card shadow="hover" class="compact-card unpallet-card">
|
<div slot="header" class="compact-header">
|
<span><i class="el-icon-tickets"></i> 未组盘条码</span>
|
<span class="list-actions">
|
<el-tag type="primary" size="small"
|
>未组盘 {{ totalStockCount }}</el-tag
|
>
|
</span>
|
</div>
|
|
<div class="table-container">
|
<el-table
|
:data="unpalletMaterials"
|
stripe
|
style="width: 100%"
|
height="100%"
|
size="small"
|
v-loading="unpalletBarcodesLoading"
|
>
|
<el-table-column
|
type="index"
|
label="序号"
|
width="60"
|
align="center"
|
></el-table-column>
|
<el-table-column
|
prop="barcode"
|
label="条码"
|
min-width="140"
|
show-overflow-tooltip
|
></el-table-column>
|
<el-table-column
|
prop="materielCode"
|
label="物料编码"
|
min-width="150"
|
show-overflow-tooltip
|
></el-table-column>
|
<el-table-column
|
prop="batchNo"
|
label="批次"
|
min-width="150"
|
show-overflow-tooltip
|
></el-table-column>
|
<el-table-column
|
prop="orderQuantity"
|
label="数量"
|
min-width="130"
|
align="right"
|
></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"
|
show-overflow-tooltip
|
></el-table-column>
|
</el-table>
|
</div>
|
</el-card>
|
</div>
|
|
<!-- 物料列表 - 固定高度带滚动条 -->
|
<div class="material-list compact">
|
<el-card shadow="hover" class="compact-card">
|
<div slot="header" class="compact-header">
|
<span><i class="el-icon-tickets"></i> 组盘数据</span>
|
<span class="list-actions">
|
<el-tag type="primary" size="small"
|
>共 {{ materials.length }} 条</el-tag
|
>
|
<el-tag type="primary" size="small"
|
>未入库数量 {{ totalStockSum }}{{ uniqueUnit }}</el-tag
|
>
|
<el-tag v-if="trayBarcode" type="success" size="small"
|
>托盘: {{ trayBarcode }}</el-tag
|
>
|
<el-tag v-if="form.warehouseType" type="info" size="small"
|
>仓库: {{ currentWarehouseName }}</el-tag
|
>
|
<el-tag v-if="form.locationType" type="info" size="small"
|
>区域: {{ currentLocationDesc }}</el-tag
|
>
|
</span>
|
</div>
|
|
<div v-if="materials.length === 0" class="empty-state compact">
|
<i class="el-icon-document"></i>
|
<p v-if="!form.warehouseType">请先选择仓库</p>
|
<p v-if="form.warehouseType && !form.locationType">
|
请先选择仓库区域
|
</p>
|
<p v-else-if="!trayBarcode">请先输入料箱条码</p>
|
<p v-else>暂无物料数据,请扫描或输入物料条码</p>
|
</div>
|
|
<div class="table-container" v-else>
|
<el-table
|
:data="materials"
|
stripe
|
style="width: 100%"
|
height="100%"
|
size="small"
|
>
|
<el-table-column
|
type="index"
|
label="序号"
|
width="60"
|
align="center"
|
></el-table-column>
|
<el-table-column
|
prop="barcode"
|
label="条码"
|
min-width="140"
|
show-overflow-tooltip
|
></el-table-column>
|
<el-table-column
|
prop="materielCode"
|
label="物料编码"
|
min-width="150"
|
show-overflow-tooltip
|
></el-table-column>
|
<el-table-column
|
prop="batchNo"
|
label="批次"
|
min-width="150"
|
show-overflow-tooltip
|
></el-table-column>
|
<el-table-column
|
prop="stockQuantity"
|
label="数量"
|
min-width="130"
|
align="right"
|
></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"
|
show-overflow-tooltip
|
></el-table-column>
|
<el-table-column
|
prop="warehouseType"
|
label="仓库"
|
min-width="120"
|
show-overflow-tooltip
|
></el-table-column>
|
</el-table>
|
</div>
|
</el-card>
|
</div>
|
</div>
|
<template #footer>
|
<el-button type="danger" size="small" @click="handleDialogClose()"
|
>关闭</el-button
|
>
|
</template>
|
</vol-box>
|
</template>
|
|
<script>
|
import http from "@/api/http.js";
|
import VolBox from "@/components/basic/VolBox.vue";
|
|
// 防抖函数
|
function debounce(func, wait) {
|
let timeout;
|
return function () {
|
const context = this;
|
const args = arguments;
|
clearTimeout(timeout);
|
timeout = setTimeout(() => {
|
func.apply(context, args);
|
}, wait);
|
};
|
}
|
|
export default {
|
components: { VolBox },
|
data() {
|
return {
|
show: false,
|
orderNo: "",
|
palletVisible: this.visible,
|
trayBarcodeReg: /^[A-Z]\d{9}$/,
|
trayBarcode: "",
|
barcode: "",
|
materials: [],
|
loading: false,
|
error: "",
|
debugMode: false,
|
currentFocus: "warehouse",
|
|
unpalletBarcodes: [],
|
unpalletBarcodesLoading: false,
|
unpalletMaterials: [],
|
|
scanCode: "",
|
lastKeyTime: null,
|
isManualInput: false,
|
isScanning: false,
|
scanTimer: null,
|
manualInputTimer: null,
|
scanTarget: "tray",
|
isSubmitting: false,
|
palletGroupedBarcodes: {},
|
|
audioConfig: {
|
successPath: require("@/assets/audio/success.mp3"),
|
errorPath: require("@/assets/audio/error.mp3"),
|
},
|
// 缓存音频实例,避免重复创建
|
successAudio: null,
|
errorAudio: null,
|
|
totalStockSum: 0,
|
totalStockCount: 0,
|
uniqueUnit: "",
|
sumLoading: false,
|
sumError: "",
|
warehouseTypes: [],
|
warehouseLoading: false,
|
locationTypes: [],
|
locationLoading: false,
|
form: {
|
warehouseType: null,
|
locationType: null,
|
},
|
rules: {
|
locationType: [
|
{
|
validator: this.validateLocationType,
|
trigger: "change",
|
},
|
],
|
trayBarcode: [
|
{
|
pattern: this.trayBarcodeReg,
|
message: "托盘号格式错误(需为1个大写字母+9个数字,如A000008024)",
|
trigger: "blur",
|
},
|
],
|
warehouseType: [
|
{
|
message: "请选择仓库",
|
trigger: "change",
|
},
|
],
|
},
|
|
// 新增:键盘事件监听标记
|
keyPressListenerAdded: false,
|
isDialogClosing: false,
|
};
|
},
|
|
computed: {
|
currentWarehouseName() {
|
const warehouse = this.warehouseTypes.find(
|
(item) => item.warehouseType === this.form.warehouseType
|
);
|
return warehouse ? warehouse.warehouseTypeDesc : "";
|
},
|
|
currentLocationDesc() {
|
const location = this.locationTypes.find(
|
(item) => item.locationType === this.form.locationType
|
);
|
return location ? location.locationTypeDesc : "";
|
},
|
|
debounceHandleBarcodeSubmit() {
|
return debounce(this.handleBarcodeSubmit, 500);
|
},
|
},
|
|
watch: {
|
// 监听show变量变化
|
show(newVal) {
|
if (newVal === true) {
|
console.log("弹框打开,重置数据");
|
this.isDialogClosing = false;
|
this.resetData();
|
this.$nextTick(() => {
|
setTimeout(() => {
|
this.fetchUnpalletMaterialDetails();
|
this.addKeyPressListener(); // 添加键盘事件监听
|
}, 300);
|
});
|
} else if (newVal === false && !this.isDialogClosing) {
|
console.log("弹框关闭,移除事件监听");
|
this.isDialogClosing = true;
|
this.removeKeyPressListener(); // 移除键盘事件监听
|
this.resetData();
|
}
|
},
|
|
visible(newVal, oldVal) {
|
this.palletVisible = newVal;
|
|
if (newVal === true && oldVal === false) {
|
console.log("弹框打开,重置数据");
|
this.resetData();
|
this.$nextTick(() => {
|
setTimeout(() => {
|
this.fetchUnpalletMaterialDetails();
|
}, 300);
|
});
|
}
|
|
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();
|
this.fetchUnpalletMaterialDetails();
|
}
|
},
|
},
|
|
mounted() {
|
// 不在mounted时添加监听,在弹窗打开时添加
|
},
|
|
beforeDestroy() {
|
// 确保组件销毁时移除监听
|
this.removeKeyPressListener();
|
this.clearAllTimers();
|
|
this.removeKeyPressListener();
|
this.clearAllTimers();
|
|
//销毁音频实例
|
this.successAudio = null;
|
this.errorAudio = null;
|
},
|
|
methods: {
|
/**
|
* 初始化音频实例(懒加载,只创建一次)
|
* @param {String} type - 音频类型:success/error
|
* @returns {Audio} 音频实例
|
*/
|
initAudioInstance(type) {
|
if (type === "success" && this.successAudio) {
|
return this.successAudio;
|
}
|
if (type === "error" && this.errorAudio) {
|
return this.errorAudio;
|
}
|
|
const audioPath =
|
type === "success"
|
? this.audioConfig.successPath
|
: this.audioConfig.errorPath;
|
|
const audioInstance = new Audio(audioPath);
|
|
// 缓存音频实例
|
if (type === "success") {
|
this.successAudio = audioInstance;
|
} else {
|
this.errorAudio = audioInstance;
|
}
|
|
// 音频加载失败回调(可选,用于调试)
|
audioInstance.onerror = (err) => {
|
console.error(`【${type} 音频】加载失败`, err);
|
};
|
|
return audioInstance;
|
},
|
|
/**
|
* 播放音频
|
* @param {String} type - 音频类型:success/error
|
*/
|
playAudio(type) {
|
try {
|
const audioInstance = this.initAudioInstance(type);
|
|
// 重置播放进度(避免重复播放时音频未结束)
|
audioInstance.currentTime = 0;
|
|
// 播放音频(返回 Promise 处理播放结果,兼容部分浏览器限制)
|
audioInstance.play().catch((err) => {
|
console.warn("音频播放失败(可能是浏览器自动播放策略限制)", err);
|
});
|
} catch (err) {
|
console.error("播放音频时发生异常", err);
|
}
|
},
|
|
/**
|
* 播放成功音频
|
*/
|
playSuccessAudio() {
|
this.playAudio("success");
|
},
|
|
/**
|
* 播放失败音频
|
*/
|
playErrorAudio() {
|
this.playAudio("error");
|
},
|
// 添加键盘事件监听
|
addKeyPressListener() {
|
if (!this.keyPressListenerAdded) {
|
document.addEventListener("keypress", this.handleKeyPress);
|
this.keyPressListenerAdded = true;
|
console.log("键盘事件监听已添加");
|
}
|
},
|
|
// 移除键盘事件监听
|
removeKeyPressListener() {
|
if (this.keyPressListenerAdded) {
|
document.removeEventListener("keypress", this.handleKeyPress);
|
this.keyPressListenerAdded = false;
|
console.log("键盘事件监听已移除");
|
}
|
},
|
|
open() {
|
this.show = true;
|
this.orderNo = "";
|
this.resetData();
|
this.initLocationTypes();
|
this.initwarehouseTypes();
|
this.fetchUnpalletMaterialDetails();
|
|
// 弹窗打开时添加键盘事件监听
|
this.$nextTick(() => {
|
setTimeout(() => {
|
this.addKeyPressListener();
|
}, 100);
|
});
|
},
|
|
validateLocationType(rule, value, callback) {
|
if (!this.form.warehouseType) {
|
callback(new Error("请先选择仓库"));
|
} else if (value === null || value === undefined || value === "") {
|
callback(new Error("请选择仓库区域"));
|
} else {
|
callback();
|
}
|
},
|
|
// 获取未组盘物料详情
|
fetchUnpalletMaterialDetails() {
|
this.unpalletBarcodesLoading = true;
|
|
http
|
.post(
|
"/api/InboundOrder/UnPalletGroupBarcode?orderNo=" + this.orderNo,
|
{}
|
)
|
.then((response) => {
|
if (response.status && Array.isArray(response.data)) {
|
this.unpalletMaterials = response.data;
|
this.unpalletBarcodes = response.data.map(
|
(item) => item.barcode || ""
|
);
|
this.totalStockCount = response.data.length;
|
} else {
|
this.unpalletMaterials = [];
|
}
|
})
|
.catch((err) => {
|
console.error("获取未组盘物料失败:", err);
|
this.unpalletMaterials = this.unpalletBarcodes.map((barcode) => ({
|
barcode: barcode,
|
materielCode: "-",
|
batchNo: "-",
|
stockQuantity: "-",
|
unit: "-",
|
supplyCode: "-",
|
warehouseType: "-",
|
}));
|
})
|
.finally(() => {
|
this.unpalletBarcodesLoading = false;
|
});
|
},
|
|
// 初始化仓库区域类型
|
initLocationTypes() {
|
this.locationLoading = true;
|
|
this.http
|
.post("api/LocationInfo/GetLocationTypes")
|
.then(({ data }) => {
|
this.locationTypes = data;
|
})
|
.catch((e) => {
|
console.error("获取区域类型失败:", e);
|
this.$message.error("获取区域类型失败");
|
})
|
.finally(() => {
|
this.locationLoading = false;
|
});
|
},
|
|
// 初始化仓库类型
|
initwarehouseTypes() {
|
this.warehouseLoading = true;
|
|
this.http
|
.post("api/Warehouse/GetwarehouseTypes")
|
.then(({ data }) => {
|
this.warehouseTypes = data;
|
})
|
.catch((e) => {
|
console.error("获取仓库类型失败:", e);
|
this.$message.error("获取仓库类型失败");
|
})
|
.finally(() => {
|
this.warehouseLoading = false;
|
});
|
},
|
|
fetchLocationByWarehouse(warehouseCode) {
|
if (!warehouseCode) {
|
this.form.locationType = null;
|
return;
|
}
|
this.locationLoading = true;
|
http.post(`/api/InboundOrder/GetLocationType?code=${warehouseCode}`)
|
.then(({ data }) => {
|
if (data) {
|
this.form.locationType = data.locationType || data;
|
if (!this.locationTypes.find(item => item.locationType === this.form.locationType)) {
|
this.locationTypes.push({
|
locationType: this.form.locationType,
|
locationTypeDesc: this.form.locationType
|
});
|
}
|
}
|
})
|
.catch((err) => {
|
console.error("获取仓库区域失败:", err);
|
this.$message.error("获取仓库区域失败,请重试");
|
this.form.locationType = null;
|
})
|
.finally(() => {
|
this.locationLoading = false;
|
});
|
},
|
// 获取库存统计
|
fetchStockStatistics(orderNo) {
|
if (!orderNo) {
|
this.sumError = "单据号为空,无法统计";
|
return Promise.resolve(null);
|
}
|
|
this.sumLoading = true;
|
this.sumError = "";
|
|
return http
|
.post("/api/InboundOrder/UnPalletQuantity?orderNo=" + orderNo, {})
|
.then((response) => {
|
if (response.data) {
|
this.totalStockSum = response.data.stockSumQuantity || 0;
|
this.totalStockCount = response.data.stockCount || 0;
|
this.uniqueUnit = response.data.uniqueUnit || "";
|
}
|
return response.data;
|
})
|
.catch((err) => {
|
console.error("统计加载失败:", err);
|
this.sumError = "统计加载失败";
|
this.totalStockSum = 0;
|
this.totalStockCount = 0;
|
throw err;
|
})
|
.finally(() => {
|
this.sumLoading = false;
|
});
|
},
|
|
// 表单验证
|
validateForm() {
|
return new Promise((resolve) => {
|
if (!this.$refs.locationForm) {
|
this.error = "表单未初始化";
|
this.$message.warning("请先选择仓库区域");
|
resolve(false);
|
return;
|
}
|
|
this.$refs.locationForm.validate((valid) => {
|
if (valid) {
|
this.error = "";
|
resolve(true);
|
} else {
|
if (!this.form.warehouseType) {
|
this.error = "请先选择仓库";
|
} else if (!this.form.locationType) {
|
this.error = "请先选择仓库区域";
|
} else {
|
this.error = "请检查表单填写是否正确";
|
}
|
resolve(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";
|
inputEl.select();
|
}
|
}
|
},
|
|
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";
|
inputEl.select();
|
}
|
}
|
},
|
|
resetData() {
|
this.palletGroupedBarcodes = {};
|
this.isSubmitting = false;
|
this.trayBarcode = "";
|
this.barcode = "";
|
this.materials = [];
|
this.unpalletBarcodes = [];
|
this.unpalletMaterials = [];
|
this.loading = false;
|
this.error = "";
|
this.scanCode = "";
|
this.lastKeyTime = null;
|
this.isManualInput = false;
|
this.isScanning = false;
|
this.currentFocus = "warehouse";
|
this.scanTarget = "tray";
|
this.clearAllTimers();
|
this.totalStockSum = 0;
|
this.totalStockCount = 0;
|
this.sumLoading = false;
|
this.sumError = "";
|
this.form = {
|
warehouseType: null,
|
locationType: null,
|
};
|
this.warehouseTypes = [];
|
this.locationTypes = [];
|
|
this.$nextTick(() => {
|
if (this.$refs.locationForm) {
|
this.$refs.locationForm.clearValidate();
|
}
|
});
|
},
|
|
clearAllTimers() {
|
if (this.manualInputTimer) {
|
clearTimeout(this.manualInputTimer);
|
this.manualInputTimer = null;
|
}
|
if (this.scanTimer) {
|
clearTimeout(this.scanTimer);
|
this.scanTimer = null;
|
}
|
},
|
|
handleDialogClose() {
|
// 先移除键盘事件监听
|
this.removeKeyPressListener();
|
this.show = false;
|
this.resetData();
|
},
|
|
// 确认按钮
|
handleConfirm() {
|
this.validateForm().then((valid) => {
|
if (!valid) return;
|
|
if (this.materials.length === 0) {
|
this.$message.warning("请至少添加一个物料");
|
return;
|
}
|
|
if (!this.trayBarcode) {
|
this.$message.warning("请输入托盘条码");
|
return;
|
}
|
|
const result = {
|
warehouseType: this.form.warehouseType,
|
warehouseName: this.currentWarehouseName,
|
locationType: this.form.locationType,
|
locationDesc: this.currentLocationDesc,
|
trayBarcode: this.trayBarcode,
|
materials: this.materials,
|
docNo: this.docNo,
|
};
|
|
this.$emit("back-success", result);
|
this.palletVisible = false;
|
});
|
},
|
|
handleTrayInput() {
|
this.isManualInput = true;
|
this.isScanning = false;
|
|
if (this.trayBarcode) {
|
this.trayBarcode = this.trayBarcode.replace(/\s+/g, "");
|
}
|
|
if (this.manualInputTimer) {
|
clearTimeout(this.manualInputTimer);
|
}
|
|
this.manualInputTimer = setTimeout(() => {
|
this.isManualInput = false;
|
}, 1000);
|
},
|
|
handleBarcodeInput() {
|
this.isManualInput = true;
|
this.isScanning = false;
|
|
if (this.barcode) {
|
this.barcode = this.barcode.replace(/\s+/g, "");
|
}
|
|
if (this.manualInputTimer) {
|
clearTimeout(this.manualInputTimer);
|
}
|
|
this.manualInputTimer = setTimeout(() => {
|
this.isManualInput = false;
|
}, 1000);
|
},
|
|
// 处理托盘条码提交
|
handleTraySubmit() {
|
this.barcode = "";
|
this.materials = [];
|
this.error = "";
|
|
if (!this.form.warehouseType) {
|
this.error = "请先选择仓库";
|
return;
|
}
|
if (!this.form.locationType) {
|
this.error = "请先选择仓库区域";
|
return;
|
}
|
|
this.validateForm().then((valid) => {
|
if (!valid) return;
|
|
let currentTrayBarcode = (this.trayBarcode || "").replace(/\s+/g, "").trim();
|
|
this.trayBarcode = currentTrayBarcode;
|
|
if (!currentTrayBarcode) {
|
this.error = "请输入或扫描托盘条码";
|
return;
|
}
|
|
this.error = "";
|
|
if (!this.trayBarcodeReg.test(currentTrayBarcode)) {
|
this.$message("托盘号格式错误");
|
this.focusTrayInput();
|
return;
|
}
|
|
this.focusBarcodeInput();
|
this.$message.success(`托盘条码已设置: ${currentTrayBarcode}`);
|
});
|
},
|
|
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;
|
},
|
|
// 处理物料条码提交
|
handleBarcodeSubmit() {
|
if (this.isSubmitting) {
|
this.$message.warning("正在处理中,请稍候");
|
return;
|
}
|
|
const currentBarcode = this.barcode.trim();
|
const currentTrayGrouped =
|
this.palletGroupedBarcodes[this.trayBarcode] || [];
|
|
if (currentTrayGrouped.includes(currentBarcode)) {
|
this.error = `条码 ${currentBarcode} 已被当前托盘组盘,请勿重复操作`;
|
this.barcode = "";
|
this.focusBarcodeInput();
|
this.playErrorAudio();
|
return;
|
}
|
|
this.isSubmitting = true;
|
|
this.validateForm()
|
.then((valid) => {
|
if (!valid) {
|
this.isSubmitting = false;
|
return;
|
}
|
|
if (!this.trayBarcode) {
|
this.error = "请先输入托盘条码";
|
this.focusTrayInput();
|
this.isSubmitting = false;
|
return;
|
}
|
|
if (!currentBarcode) {
|
this.error = "请输入或扫描物料条码";
|
this.isSubmitting = false;
|
return;
|
}
|
|
this.focusBarcodeInput();
|
this.error = "";
|
this.loading = true;
|
|
console.log("组盘请求参数:", {
|
palletCode: this.trayBarcode,
|
barcode: currentBarcode,
|
locationTypeDesc: this.currentLocationDesc,
|
locationType: this.form.locationType,
|
warehouseType: this.form.warehouseType,
|
});
|
|
return this.fetchMaterialData(currentBarcode);
|
})
|
.then((materialData) => {
|
if (!materialData || materialData.length === 0) {
|
return;
|
}
|
|
this.materials = [];
|
const newBarcodes = [];
|
|
materialData.forEach((item) => {
|
this.materials.push({
|
...item,
|
trayCode: this.trayBarcode,
|
locationType: this.form.locationType,
|
locationDesc: this.currentLocationDesc,
|
scanTime: this.formatTime(new Date()),
|
});
|
newBarcodes.push(item.barcode);
|
});
|
|
if (!this.palletGroupedBarcodes[this.trayBarcode]) {
|
this.palletGroupedBarcodes[this.trayBarcode] = [];
|
}
|
this.palletGroupedBarcodes[this.trayBarcode] = [
|
...new Set([
|
...this.palletGroupedBarcodes[this.trayBarcode],
|
...newBarcodes,
|
]),
|
];
|
|
this.orderNo = materialData[0].orderNo;
|
|
return Promise.all([
|
this.fetchStockStatistics(materialData[0].orderNo),
|
this.fetchUnpalletMaterialDetails(),
|
]);
|
})
|
.then(() => {
|
this.barcode = "";
|
this.scanCode = "";
|
this.isScanning = false;
|
|
setTimeout(() => {
|
this.focusBarcodeInput();
|
}, 100);
|
})
|
.catch((err) => {
|
console.error("处理物料条码失败:", err);
|
this.error = err.message || "查询条码信息失败,请重试";
|
this.focusBarcodeInput();
|
setTimeout(() => {
|
const inputEl =
|
this.$refs.barcodeInput?.$el?.querySelector("input");
|
if (inputEl) inputEl.select();
|
}, 100);
|
})
|
.finally(() => {
|
this.loading = false;
|
this.isSubmitting = false;
|
});
|
},
|
|
// API请求
|
fetchMaterialData(barcode) {
|
return http
|
.post("/api/Inbound/GroupPallet", {
|
palletCode: this.trayBarcode,
|
barcode: barcode,
|
locationTypeDesc: this.currentLocationDesc,
|
locationType: this.form.locationType,
|
warehouseType: this.form.warehouseType,
|
})
|
.then((response) => {
|
let materialData;
|
|
if (typeof response.data === "string") {
|
try {
|
materialData = JSON.parse(response.data);
|
} catch (e) {
|
console.error("解析响应数据失败:", e);
|
this.playErrorAudio(); // 解析失败播放错误音
|
throw new Error("响应数据格式错误");
|
}
|
} else {
|
materialData = response.data;
|
}
|
|
if (!response.status) {
|
this.error = response.message || "查询条码信息失败,请重试";
|
this.playErrorAudio(); // 接口返回失败播放错误音
|
return [];
|
}
|
|
this.playSuccessAudio(); // 接口返回成功播放成功音
|
return materialData || [];
|
})
|
.catch((error) => {
|
console.error("API调用失败:", error);
|
this.$message.error("接口请求失败,请联系管理员");
|
this.playErrorAudio(); // 请求异常播放错误音
|
throw error;
|
});
|
},
|
|
// 处理扫码枪输入
|
handleKeyPress(event) {
|
// 检查弹窗是否显示
|
if (!this.show || this.isDialogClosing) {
|
return;
|
}
|
|
if (this.isManualInput || this.isSubmitting) {
|
return;
|
}
|
|
const key = event.key;
|
const currentTime = new Date().getTime();
|
|
if (key === "Enter") {
|
event.preventDefault();
|
if (this.scanCode.length > 0) {
|
if (this.scanTarget === "material" && !this.trayBarcode) {
|
this.$message.warning("请先设置托盘条码");
|
this.scanCode = "";
|
this.lastKeyTime = null;
|
return;
|
}
|
|
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;
|
},
|
|
// 格式化时间
|
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}`;
|
},
|
|
// 仓库切换事件
|
handleWarehouseChange() {
|
this.form.locationType = null;
|
this.trayBarcode = "";
|
this.barcode = "";
|
this.materials = [];
|
this.error = "";
|
this.fetchLocationByWarehouse(this.form.warehouseType);
|
},
|
|
// 区域切换事件
|
handleLocationChange() {
|
this.trayBarcode = "";
|
this.barcode = "";
|
this.materials = [];
|
this.error = "";
|
},
|
},
|
};
|
</script>
|
|
<style scoped>
|
.barcode-scanner-container {
|
max-width: 1200px;
|
margin: 0 auto;
|
padding: 10px;
|
display: flex;
|
flex-direction: column;
|
height: 100%;
|
gap: 8px;
|
}
|
|
/* 紧凑布局样式 */
|
.compact {
|
margin-bottom: 0;
|
}
|
|
.compact-form {
|
margin-bottom: 0;
|
}
|
|
.compact-item {
|
margin-bottom: 0;
|
}
|
|
.compact-card {
|
margin-bottom: 0;
|
}
|
|
.compact-card >>> .el-card__body {
|
padding: 12px;
|
}
|
|
.compact-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
padding: 0 !important;
|
}
|
|
.compact-header >>> .el-card__header {
|
padding: 8px 12px;
|
}
|
|
.compact-input {
|
margin: 8px 0;
|
}
|
|
.compact-tips {
|
margin-top: 8px;
|
font-size: 11px;
|
}
|
|
/* 仓库区域选择 - 紧凑 */
|
.location-section.compact {
|
margin-bottom: 8px;
|
}
|
|
.location-section.compact >>> .el-form-item {
|
margin-bottom: 0;
|
}
|
|
/* 托盘信息 - 紧凑 */
|
.tray-info.compact {
|
padding: 6px 10px;
|
margin-bottom: 8px;
|
font-size: 13px;
|
}
|
|
/* 扫码区域 - 紧凑 */
|
.input-section.compact {
|
margin-bottom: 8px;
|
flex-shrink: 0;
|
}
|
|
/* 物料列表 - 固定高度带滚动 */
|
.material-list.compact {
|
flex: 1;
|
min-height: 0;
|
/* 重要:允许flex子项收缩 */
|
display: flex;
|
flex-direction: column;
|
}
|
|
.material-list.compact >>> .el-card {
|
display: flex;
|
flex-direction: column;
|
height: 100%;
|
}
|
|
.material-list.compact >>> .el-card__body {
|
flex: 1;
|
display: flex;
|
flex-direction: column;
|
padding: 0;
|
min-height: 0;
|
}
|
|
.table-container {
|
flex: 1;
|
min-height: 0;
|
overflow: hidden;
|
}
|
|
.material-list.compact >>> .el-table {
|
flex: 1;
|
}
|
|
.material-list.compact >>> .el-table__body-wrapper {
|
overflow-y: auto;
|
}
|
|
/* 紧凑的空状态 */
|
.empty-state.compact {
|
padding: 20px 0;
|
flex: 1;
|
display: flex;
|
flex-direction: column;
|
justify-content: center;
|
align-items: center;
|
}
|
|
.empty-state.compact i {
|
font-size: 36px;
|
margin-bottom: 8px;
|
}
|
|
.empty-state.compact p {
|
font-size: 13px;
|
}
|
|
/* 其他原有样式调整 */
|
.page-title {
|
text-align: center;
|
margin-bottom: 15px;
|
}
|
|
.scan-status {
|
font-size: 12px;
|
color: #67c23a;
|
}
|
|
.scan-indicator {
|
display: inline-block;
|
width: 8px;
|
height: 8px;
|
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;
|
}
|
|
.input-tips {
|
margin-top: 6px;
|
color: #909399;
|
}
|
|
.warning-text {
|
color: #e6a23c;
|
font-weight: bold;
|
}
|
|
.loading.compact {
|
text-align: center;
|
margin: 10px 0;
|
padding: 5px;
|
}
|
|
.loading.compact p {
|
margin-top: 5px;
|
color: #409eff;
|
font-size: 12px;
|
}
|
|
.error-message.compact {
|
margin: 5px 0;
|
}
|
|
.error-message.compact >>> .el-alert {
|
padding: 6px 12px;
|
}
|
|
.list-actions {
|
display: flex;
|
align-items: center;
|
gap: 4px;
|
}
|
|
.list-actions >>> .el-tag {
|
height: 24px;
|
line-height: 22px;
|
padding: 0 6px;
|
}
|
|
.clear-all-btn {
|
margin-left: 8px;
|
}
|
|
.material-code {
|
font-family: "Courier New", monospace;
|
font-weight: bold;
|
color: #409eff;
|
}
|
|
.location-info {
|
color: #606266;
|
font-weight: normal;
|
}
|
|
.debug-info {
|
background: #f5f7fa;
|
padding: 8px;
|
border-radius: 4px;
|
margin-top: 8px;
|
font-size: 11px;
|
color: #909399;
|
}
|
|
.small-button {
|
padding: 6px 8px;
|
font-size: 11px;
|
}
|
|
/* 输入框组样式调整 */
|
.custom-input-group {
|
display: flex;
|
align-items: center;
|
width: 100%;
|
margin: 8px 0;
|
border: 1px solid #dcdfe6;
|
border-radius: 4px;
|
overflow: hidden;
|
background: #fff;
|
}
|
|
.input-label {
|
padding: 0 12px;
|
background: #f5f7fa;
|
border-right: 1px solid #dcdfe6;
|
color: #606266;
|
font-size: 13px;
|
white-space: nowrap;
|
height: 36px;
|
line-height: 36px;
|
flex-shrink: 0;
|
min-width: 70px;
|
text-align: center;
|
}
|
|
.input-container {
|
display: flex;
|
flex: 1;
|
align-items: center;
|
}
|
|
.custom-input {
|
flex: 1;
|
}
|
|
.custom-input >>> .el-input__inner {
|
border: none;
|
border-radius: 0;
|
height: 36px;
|
line-height: 36px;
|
font-size: 13px;
|
}
|
|
/* 响应式调整 */
|
@media (max-width: 768px) {
|
.barcode-scanner-container {
|
padding: 5px;
|
}
|
|
.custom-input-group {
|
flex-direction: column;
|
border: none;
|
}
|
|
.input-label {
|
width: 100%;
|
border-right: none;
|
border-bottom: 1px solid #dcdfe6;
|
margin-bottom: 5px;
|
}
|
|
.input-container {
|
width: 100%;
|
border: 1px solid #dcdfe6;
|
border-radius: 4px;
|
}
|
|
.unpallet-section.compact {
|
margin-bottom: 8px;
|
flex-shrink: 0;
|
}
|
|
.unpallet-card {
|
flex-shrink: 0;
|
}
|
|
.unpallet-barcode-list {
|
display: flex;
|
flex-wrap: wrap;
|
gap: 6px;
|
padding: 8px 0;
|
max-height: 180px;
|
overflow-y: auto;
|
}
|
|
.unpallet-barcode-list >>> .el-tag {
|
cursor: pointer;
|
max-width: calc(33.333% - 4px);
|
overflow: hidden;
|
text-overflow: ellipsis;
|
white-space: nowrap;
|
}
|
|
@media (max-width: 768px) {
|
.unpallet-barcode-list >>> .el-tag {
|
max-width: calc(50% - 4px);
|
}
|
}
|
}
|
</style>
|