<template>
|
<div>
|
<vol-box
|
v-model="showDetailBox"
|
:lazy="true"
|
width="65%"
|
:padding="20"
|
title="虚拟出入库"
|
class="custom-vol-box"
|
>
|
<div>
|
<!-- 单据选择区域(可输入搜索) -->
|
<el-form :inline="true" :model="orderForm" style="margin-bottom: 20px; align-items: flex-end;">
|
<el-form-item label="出库单据:" name="outboundOrderId">
|
<el-select
|
v-model="orderForm.outboundOrderId"
|
placeholder="请选择或扫描出库单据号"
|
clearable
|
style="width: 220px; margin-right: 10px;"
|
@change="handleOrderChange"
|
@visible-change="handleOutboundVisibleChange"
|
:loading="loading"
|
filterable
|
:filter-method="filterOutboundOrders"
|
@input="handleOutboundScanInput"
|
ref="outboundSelectRef"
|
>
|
<el-option
|
v-for="order in filteredOutboundOrders"
|
:key="order.id"
|
:label="order.orderNo"
|
:value="order.id"
|
></el-option>
|
</el-select>
|
</el-form-item>
|
<el-form-item label="采购单据:" name="purchaseOrderId">
|
<el-select
|
v-model="orderForm.purchaseOrderId"
|
placeholder="请选择或扫描采购单据号"
|
clearable
|
style="width: 220px; margin-right: 10px;"
|
@change="handleOrderChange"
|
@visible-change="handlePurchaseVisibleChange"
|
:loading="loading"
|
filterable
|
:filter-method="filterPurchaseOrders"
|
@input="handlePurchaseScanInput"
|
ref="purchaseSelectRef"
|
>
|
<el-option
|
v-for="order in filteredPurchaseOrders"
|
:key="order.id"
|
:label="order.orderNo"
|
:value="order.id"
|
></el-option>
|
</el-select>
|
</el-form-item>
|
</el-form>
|
|
<!-- 上方输入框 -->
|
<el-form :inline="true" :model="formData" ref="formRef" style="margin-bottom: 20px; align-items: flex-end;">
|
<el-form-item
|
label="扫描条码:"
|
style="width: 80%"
|
name="barcode"
|
:rules="[{ required: true, message: '请扫描或输入条码', trigger: 'blur' }]"
|
>
|
<el-input
|
ref="barcodeInputRef"
|
v-model="formData.barcode"
|
placeholder="请使用扫码枪扫描条码,或手动输入"
|
@keyup.enter="handleScan"
|
autofocus
|
class="custom-input"
|
:disabled="!orderForm.outboundOrderId || !orderForm.purchaseOrderId"
|
></el-input>
|
</el-form-item>
|
<el-form-item>
|
<el-button
|
type="primary"
|
size="small"
|
@click="handleScan"
|
class="custom-button"
|
:disabled="!orderForm.outboundOrderId || !orderForm.purchaseOrderId || loading"
|
>
|
<Search /> 确认扫描
|
</el-button>
|
</el-form-item>
|
</el-form>
|
|
<!-- 下方显示框 -->
|
<div class="scan-list">
|
<el-card shadow="hover" style="margin-bottom: 10px; border: none;" class="custom-card">
|
<div class="card-header">
|
<span class="header-title">已扫描条码列表(共{{ scannedBarcodes.length }}条)</span>
|
</div>
|
<div class="card-body">
|
<el-scrollbar height="400px" class="custom-scrollbar">
|
<transition-group name="barcode-item-transition">
|
<div class="barcode-item" v-for="(item, index) in scannedBarcodes" :key="item.barcode" :data-index="index">
|
<span class="barcode-text">{{ index + 1 }}. {{ item.barcode }}</span>
|
<el-button
|
class="delete-btn"
|
@click="removeItem(index, item.barcode)"
|
icon="Delete"
|
circle
|
:disabled="loading"
|
></el-button>
|
</div>
|
</transition-group>
|
<div class="empty-tip" v-if="scannedBarcodes.length === 0">
|
<span>暂无扫描记录,请先选择单据后扫描条码</span>
|
</div>
|
</el-scrollbar>
|
</div>
|
</el-card>
|
</div>
|
</div>
|
|
<template #footer>
|
<div class="footer-actions">
|
<el-button
|
type="primary"
|
size="small"
|
@click="submit"
|
:disabled="scannedBarcodes.length === 0 || !orderForm.outboundOrderId || !orderForm.purchaseOrderId || loading"
|
class="submit-btn"
|
>
|
<Check /> 提交出库
|
</el-button>
|
<el-button type="text" size="small" @click="showDetailBox = false" class="cancel-btn" :disabled="loading">
|
取消
|
</el-button>
|
</div>
|
</template>
|
</vol-box>
|
</div>
|
</template>
|
|
<script setup>
|
import { ref, reactive, onMounted, nextTick } from 'vue';
|
import { ElMessage } from 'element-plus';
|
|
import VolBox from "@/components/basic/VolBox.vue";
|
import http from '@/api/http';
|
|
// 响应式数据
|
const showDetailBox = ref(false);
|
const orderForm = reactive({
|
outboundOrderId: "",
|
purchaseOrderId: ""
|
});
|
const formData = reactive({
|
barcode: "",
|
});
|
const scannedBarcodes = ref([]);
|
const outboundOrders = ref([]);
|
const purchaseOrders = ref([]);
|
const filteredOutboundOrders = ref([]);
|
const filteredPurchaseOrders = ref([]);
|
const loading = ref(false);
|
|
// 模板引用
|
const formRef = ref(null);
|
const barcodeInputRef = ref(null);
|
const outboundSelectRef = ref(null); // 新增:出库单select的ref
|
const purchaseSelectRef = ref(null); // 新增:采购单select的ref
|
|
// 用于防止输入事件和change事件冲突的锁
|
const isProcessingScan = ref(false);
|
|
// 组件挂载时初始化过滤后的列表
|
onMounted(() => {
|
filteredOutboundOrders.value = [...outboundOrders.value];
|
filteredPurchaseOrders.value = [...purchaseOrders.value];
|
});
|
|
// 打开弹窗
|
const open = () => {
|
showDetailBox.value = true;
|
scannedBarcodes.value = [];
|
formData.barcode = "";
|
orderForm.outboundOrderId = "";
|
orderForm.purchaseOrderId = "";
|
nextTick(() => {
|
barcodeInputRef.value?.focus();
|
});
|
};
|
|
// 加载出库单据列表
|
const loadOutboundOrders = async () => {
|
if (outboundOrders.value.length > 0) return;
|
try {
|
loading.value = true;
|
const res = await http.get("/api/OutboundPicking/GetAvailablePickingOrders");
|
|
if (res.code === 0) {
|
outboundOrders.value = res.data.map(orderNo => ({
|
id: orderNo,
|
orderNo: orderNo
|
}));
|
filteredOutboundOrders.value = [...outboundOrders.value];
|
} else {
|
ElMessage.error("加载出库单据失败:" + (res.message || '未知错误'));
|
}
|
} catch (error) {
|
ElMessage.error("加载出库单据异常:" + error.message);
|
} finally {
|
loading.value = false;
|
}
|
};
|
|
// 加载采购单据列表
|
const loadPurchaseOrders = async () => {
|
if (purchaseOrders.value.length > 0) return;
|
try {
|
loading.value = true;
|
const res = await http.get("/api/OutboundPicking/GetAvailablePurchaseOrders");
|
|
if (res.status === true) {
|
purchaseOrders.value = res.data.map(orderNo => ({
|
id: orderNo,
|
orderNo: orderNo
|
}));
|
filteredPurchaseOrders.value = [...purchaseOrders.value];
|
} else {
|
ElMessage.error("加载采购单据失败:" + (res.message || '未知错误'));
|
}
|
} catch (error) {
|
ElMessage.error("加载采购单据异常:" + error.message);
|
} finally {
|
loading.value = false;
|
}
|
};
|
|
// 出库单据过滤方法
|
const filterOutboundOrders = (value) => {
|
if (!value) {
|
filteredOutboundOrders.value = [...outboundOrders.value];
|
} else {
|
const lowerValue = value.toLowerCase();
|
filteredOutboundOrders.value = outboundOrders.value.filter(order =>
|
order.orderNo.toLowerCase().includes(lowerValue)
|
);
|
}
|
};
|
|
// 采购单据过滤方法
|
const filterPurchaseOrders = (value) => {
|
if (!value) {
|
filteredPurchaseOrders.value = [...purchaseOrders.value];
|
} else {
|
const lowerValue = value.toLowerCase();
|
filteredPurchaseOrders.value = purchaseOrders.value.filter(order =>
|
order.orderNo.toLowerCase().includes(lowerValue)
|
);
|
}
|
};
|
|
// 出库单据下拉框显示/隐藏时触发
|
const handleOutboundVisibleChange = (visible) => {
|
if (visible) {
|
loadOutboundOrders();
|
} else {
|
// 当下拉框关闭时,如果是扫描操作导致的,清空输入框
|
if (isProcessingScan.value) {
|
nextTick(() => {
|
outboundSelectRef.value?.clearInput();
|
isProcessingScan.value = false;
|
});
|
}
|
}
|
};
|
|
// 采购单据下拉框显示/隐藏时触发
|
const handlePurchaseVisibleChange = (visible) => {
|
if (visible) {
|
loadPurchaseOrders();
|
} else {
|
// 当下拉框关闭时,如果是扫描操作导致的,清空输入框
|
if (isProcessingScan.value) {
|
nextTick(() => {
|
purchaseSelectRef.value?.clearInput();
|
isProcessingScan.value = false;
|
});
|
}
|
}
|
};
|
|
// 单据选择变化时触发
|
const handleOrderChange = () => {
|
// 如果是手动选择,则不清空输入框
|
isProcessingScan.value = false;
|
|
scannedBarcodes.value = [];
|
nextTick(() => {
|
barcodeInputRef.value?.focus();
|
});
|
};
|
|
/**
|
* 处理出库单扫描输入
|
* @param {string} scanText - 扫描枪输入的文本
|
*/
|
const handleOutboundScanInput = async (scanText) => {
|
// 避免处理空字符串或由change事件触发的内部操作
|
if (!scanText || isProcessingScan.value) return;
|
|
// 查找完全匹配的订单
|
const matchedOrder = outboundOrders.value.find(order => order.orderNo === scanText);
|
|
if (matchedOrder) {
|
isProcessingScan.value = true; // 标记为正在处理扫描
|
// 手动赋值
|
orderForm.outboundOrderId = matchedOrder.id;
|
ElMessage.success(`成功选择出库单:${matchedOrder.orderNo}`);
|
|
// 延迟聚焦到下一个输入框,确保赋值完成
|
setTimeout(() => {
|
purchaseSelectRef.value?.focus();
|
}, 100);
|
}
|
// 如果没有匹配项,不做任何事,让el-select保持过滤状态,用户可以手动选择
|
};
|
|
/**
|
* 处理采购单扫描输入
|
* @param {string} scanText - 扫描枪输入的文本
|
*/
|
const handlePurchaseScanInput = async (scanText) => {
|
if (!scanText || isProcessingScan.value) return;
|
|
const matchedOrder = purchaseOrders.value.find(order => order.orderNo === scanText);
|
|
if (matchedOrder) {
|
isProcessingScan.value = true; // 标记为正在处理扫描
|
orderForm.purchaseOrderId = matchedOrder.id;
|
ElMessage.success(`成功选择采购单:${matchedOrder.orderNo}`);
|
|
setTimeout(() => {
|
barcodeInputRef.value?.focus();
|
}, 100);
|
}
|
};
|
|
|
// 扫描条码处理
|
const handleScan = async () => {
|
if (!formRef.value) return;
|
await formRef.value.validateField('barcode');
|
|
const barcode = formData.barcode.trim();
|
|
if (scannedBarcodes.value.some(item => item.barcode === barcode)) {
|
ElMessage.warning(`条码 ${barcode} 已扫描过,请勿重复扫描`);
|
formData.barcode = "";
|
nextTick(() => barcodeInputRef.value?.focus());
|
return;
|
}
|
|
try {
|
loading.value = true;
|
const res = await http.post("/api/OutboundPicking/BarcodeValidate", {
|
outOder: orderForm.outboundOrderId,
|
inOder: orderForm.purchaseOrderId,
|
barCode: barcode
|
});
|
|
if (res.status === true) {
|
scannedBarcodes.value.push({ barcode });
|
ElMessage.success("扫描成功");
|
formData.barcode = "";
|
} else {
|
ElMessage.error("扫描失败:" + (res.message || '验证失败'));
|
}
|
} catch (error) {
|
ElMessage.error("扫描验证异常:" + error.message);
|
} finally {
|
loading.value = false;
|
nextTick(() => barcodeInputRef.value?.focus());
|
}
|
};
|
|
// 移除单条扫描记录
|
const removeItem = async (index, barcode) => {
|
try {
|
loading.value = true;
|
const res = await http.post("/api/OutboundPicking/DeleteBarcode", {
|
outOder: orderForm.outboundOrderId,
|
inOder: orderForm.purchaseOrderId,
|
barCode: barcode
|
});
|
|
if (res.status === true) {
|
scannedBarcodes.value.splice(index, 1);
|
ElMessage.success("删除成功");
|
} else {
|
ElMessage.error("删除失败:" + (res.message || '删除失败'));
|
}
|
} catch (error) {
|
ElMessage.error("删除条码异常:" + error.message);
|
} finally {
|
loading.value = false;
|
}
|
};
|
|
// 提交出库
|
const submit = async () => {
|
if (scannedBarcodes.value.length === 0) {
|
ElMessage.warning("请先扫描至少一条条码");
|
return;
|
}
|
|
const barcodes = scannedBarcodes.value.map(item => item.barcode);
|
|
try {
|
loading.value = true;
|
const res = await http.post("/api/OutboundPicking/NoStockOutSubmit", {
|
OutOderSubmit: orderForm.outboundOrderId,
|
InOderSubmit: orderForm.purchaseOrderId,
|
BarCodeSubmit: barcodes
|
});
|
|
if (res.status === true) {
|
ElMessage.success("出库提交成功");
|
showDetailBox.value = false;
|
} else {
|
ElMessage.error("出库提交失败:" + (res.message || '提交失败'));
|
}
|
} catch (error) {
|
ElMessage.error("出库提交异常:" + error.message);
|
} finally {
|
loading.value = false;
|
}
|
};
|
|
// 暴露给父组件的方法
|
defineExpose({
|
open
|
});
|
</script>
|
|
<style scoped>
|
/* 关键:定义列表项的过渡动画 */
|
.barcode-item-transition-enter-active,
|
.barcode-item-transition-leave-active {
|
transition: all 0.3s ease;
|
}
|
.barcode-item-transition-enter-from {
|
opacity: 0;
|
transform: translateY(10px);
|
}
|
.barcode-item-transition-leave-to {
|
opacity: 0;
|
transform: translateX(30px);
|
}
|
/* 确保删除时其他元素平滑上移 */
|
.barcode-item-transition-move {
|
transition: transform 1s ease;
|
}
|
|
.scan-list {
|
width: 100%;
|
}
|
|
.custom-card {
|
border-radius: 12px;
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08) !important;
|
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
}
|
.custom-card:hover {
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.12) !important;
|
}
|
|
.card-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
margin-bottom: 15px;
|
padding-bottom: 10px;
|
border-bottom: 1px solid #f0f0f0;
|
}
|
.header-title {
|
font-weight: 600;
|
font-size: 15px;
|
color: #333;
|
}
|
|
.card-body {
|
padding: 0;
|
}
|
|
/* 自定义滚动条 */
|
.custom-scrollbar :deep(.el-scrollbar__thumb) {
|
background: rgba(0, 0, 0, 0.2);
|
border-radius: 4px;
|
width: 4px;
|
}
|
.custom-scrollbar :deep(.el-scrollbar__bar:hover .el-scrollbar__thumb) {
|
background: rgba(0, 0, 0, 0.3);
|
width: 6px;
|
}
|
.custom-scrollbar :deep(.el-scrollbar__wrap) {
|
overflow-x: hidden;
|
}
|
|
.barcode-item {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
padding: 10px 15px;
|
border-bottom: 1px solid #f7f7f7;
|
transition: background-color 0.2s ease;
|
}
|
.barcode-item:hover {
|
background-color: #fafafa;
|
}
|
/* 为奇数行添加轻微的背景色,增强可读性 */
|
.barcode-item:nth-child(odd) {
|
background-color: #f9f9f9;
|
}
|
.barcode-text {
|
flex: 1;
|
font-size: 14px;
|
color: #666;
|
transition: color 0.2s;
|
}
|
.barcode-item:hover .barcode-text {
|
color: #409eff;
|
}
|
|
.delete-btn {
|
color: #ea1919;
|
font-size: 16px;
|
transition: all 0.2s;
|
opacity: 0.7;
|
}
|
.barcode-item:hover .delete-btn {
|
opacity: 1;
|
}
|
.delete-btn:hover {
|
color: #f56c6c !important;
|
background: rgba(245, 108, 108, 0.1);
|
}
|
|
.empty-tip {
|
text-align: center;
|
padding: 80px 0;
|
color: #999;
|
font-size: 14px;
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
justify-content: center;
|
gap: 15px;
|
}
|
.empty-tip i {
|
font-size: 40px;
|
color: #dcdfe6;
|
}
|
|
/* 自定义输入框 */
|
.custom-input :deep(.el-input__inner) {
|
border-radius: 6px;
|
border-color: #e4e7ed;
|
transition: all 0.3s;
|
height: 36px;
|
line-height: 36px;
|
}
|
.custom-input :deep(.el-input__inner:focus) {
|
border-color: #409eff;
|
box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.1);
|
}
|
|
/* 自定义按钮 */
|
.custom-button {
|
border-radius: 6px;
|
height: 36px;
|
line-height: 36px;
|
font-size: 13px;
|
font-weight: 500;
|
transition: all 0.2s;
|
}
|
.custom-button:hover {
|
transform: translateY(-1px);
|
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.2);
|
}
|
|
.footer-actions {
|
text-align: right;
|
}
|
.submit-btn {
|
border-radius: 6px;
|
font-weight: 500;
|
padding: 10px 20px;
|
transition: all 0.2s;
|
}
|
.submit-btn:hover {
|
transform: translateY(-1px);
|
box-shadow: 0 4px 12px rgba(46, 164, 79, 0.2);
|
}
|
.cancel-btn {
|
color: #666;
|
margin-right: 10px;
|
transition: color 0.2s;
|
}
|
.cancel-btn:hover {
|
color: #333;
|
background: #f5f5f5;
|
}
|
</style>
|
|
<style>
|
/* 全局样式部分保持不变 */
|
.text-button:hover {
|
background-color: #f0f9eb !important;
|
}
|
.el-table .warning-row {
|
background: oldlace;
|
}
|
.box-table .el-table tbody tr:hover > td {
|
background-color: #d8e0d4 !important;
|
}
|
.box-table .el-table tbody tr.current-row > td {
|
background-color: #f0f9eb !important;
|
}
|
.el-table .success-row {
|
background: #f0f9eb;
|
}
|
.box-table .el-table {
|
border: 1px solid #ebeef5;
|
}
|
.box-head .el-alert__content {
|
width: 100%;
|
}
|
</style>
|