<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="outboundOrderNo">
|
<el-input
|
v-model="orderForm.outboundOrderNo"
|
placeholder="请输入或扫描出库单据号"
|
clearable
|
style="width: 220px; margin-right: 10px;"
|
@input="handleOutboundInput"
|
@keyup.enter="focusPurchaseInput"
|
ref="outboundInputRef"
|
></el-input>
|
</el-form-item>
|
<el-form-item label="采购单据:" name="purchaseOrderNo">
|
<el-input
|
v-model="orderForm.purchaseOrderNo"
|
placeholder="请输入或扫描采购单据号"
|
clearable
|
style="width: 220px; margin-right: 10px;"
|
@input="handlePurchaseInput"
|
@keyup.enter="focusBarcodeInput"
|
ref="purchaseInputRef"
|
></el-input>
|
</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.outboundOrderNo || !orderForm.purchaseOrderNo"
|
></el-input>
|
</el-form-item>
|
<el-form-item>
|
<el-button
|
type="primary"
|
size="small"
|
@click="handleScan"
|
class="custom-button"
|
:disabled="!orderForm.outboundOrderNo || !orderForm.purchaseOrderNo || 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.outboundOrderNo || !orderForm.purchaseOrderNo || 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 { Search } from '@element-plus/icons-vue';
|
|
import VolBox from "@/components/basic/VolBox.vue";
|
import http from '@/api/http';
|
|
// 响应式数据
|
const showDetailBox = ref(false);
|
const orderForm = reactive({
|
outboundOrderNo: "",
|
purchaseOrderNo: ""
|
});
|
const formData = reactive({
|
barcode: "",
|
});
|
const scannedBarcodes = ref([]);
|
const loading = ref(false);
|
|
// 模板引用
|
const formRef = ref(null);
|
const barcodeInputRef = ref(null);
|
const outboundInputRef = ref(null);
|
const purchaseInputRef = ref(null);
|
|
// 组件挂载时聚焦到出库单输入框
|
onMounted(() => {
|
nextTick(() => {
|
outboundInputRef.value?.focus();
|
});
|
});
|
|
// 打开弹窗
|
const open = () => {
|
showDetailBox.value = true;
|
scannedBarcodes.value = [];
|
formData.barcode = "";
|
orderForm.outboundOrderNo = "";
|
orderForm.purchaseOrderNo = "";
|
nextTick(() => {
|
outboundInputRef.value?.focus();
|
});
|
};
|
|
// 出库单输入处理(扫码或手动输入)
|
const handleOutboundInput = (value) => {
|
// 扫码枪输入通常会自动触发enter事件,这里主要处理手动输入的情况
|
if (value && value.trim()) {
|
// 可以在这里添加出库单号的格式验证逻辑
|
}
|
};
|
|
// 采购单输入处理(扫码或手动输入)
|
const handlePurchaseInput = (value) => {
|
if (value && value.trim()) {
|
// 可以在这里添加采购单号的格式验证逻辑
|
}
|
};
|
|
// 焦点跳转函数
|
const focusPurchaseInput = () => {
|
if (orderForm.outboundOrderNo.trim()) {
|
purchaseInputRef.value?.focus();
|
} else {
|
ElMessage.warning("请先输入有效的出库单据号");
|
}
|
};
|
|
const focusBarcodeInput = () => {
|
if (orderForm.purchaseOrderNo.trim()) {
|
barcodeInputRef.value?.focus();
|
} else {
|
ElMessage.warning("请先输入有效的采购单据号");
|
}
|
};
|
|
// 扫描条码处理
|
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.outboundOrderNo, // 注意:这里现在传递的是单据号字符串,而不是ID
|
inOder: orderForm.purchaseOrderNo, // 注意:这里现在传递的是单据号字符串,而不是ID
|
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.outboundOrderNo,
|
inOder: orderForm.purchaseOrderNo,
|
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.outboundOrderNo,
|
InOderSubmit: orderForm.purchaseOrderNo,
|
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>
|