Files
klp-oa/klp-ui/src/views/wms/print/read.vue

361 lines
9.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="qr-parser-container">
<div class="card">
<el-alert title="请将当前输入法切换到英文输入后再进行扫码操作" type="warning"></el-alert>
<div class="card-header">
<h2 class="title">二维码解析器</h2>
<p class="subtitle">扫描或输入二维码内容进行解析</p>
</div>
<div class="input-section">
<el-input v-model="text" @input="handleInput" @blur="handleBlur" ref="textarea"
placeholder="请扫描二维码或粘贴二维码文本内容..." :rows="3" type="textarea"
:class="{ 'invalid-input': !isValid && text.length > 0 }" />
<div class="input-hint">
<i class="el-icon-info-circle"></i>
<span>{{ inputHint }}</span>
</div>
</div>
<!-- 加载状态 -->
<el-loading v-if="isLoading" text="正在解析数据..." class="loading-overlay"></el-loading>
<!-- 二维码解析结果 - 只有数据有效时才显示 -->
<div v-if="isValid" class="result-section fade-in">
<div class="result-header">
<h3>解析结果</h3>
<el-tag :type="result.ioType === 'in' ? 'success' : 'warning'" size="small">
{{ result.ioType === 'in' ? '入库' : '出库' }}
</el-tag>
</div>
<el-descriptions :column="1" border class="result-table">
<el-descriptions-item label="出入库类型" :span="1">
{{ result.ioType === 'in' ? '入库' : '出库' }}
</el-descriptions-item>
<el-descriptions-item label="物料类型" :span="1">
{{ formatItemType(result.itemType) }}
</el-descriptions-item>
<el-descriptions-item label="物料信息" :span="1">
<ProductInfo v-if="result.itemType === 'product' || result.itemType === 'semi'"
:productId="result.itemId" />
<RawMaterialInfo v-else-if="result.itemType === 'raw_material'" :materialId="result.itemId" />
<el-input v-else disabled v-model="result.itemId" placeholder="未知物料" :disabled="true"
style="width: 100%;" />
</el-descriptions-item>
<el-descriptions-item label="数量" :span="1">
{{ result.quantity }}
</el-descriptions-item>
</el-descriptions>
<div class="action-buttons">
<el-button type="primary" @click="handleSubmit" :loading="isSubmitting" icon="el-icon-check">
确认执行
</el-button>
<el-button type="default" @click="handleReset" icon="el-icon-refresh-right">
重置
</el-button>
</div>
</div>
</div>
</div>
</template>
<script>
import ProductInfo from '@/components/KLPService/Renderer/ProductInfo';
import RawMaterialInfo from '@/components/KLPService/Renderer/RawMaterialInfo';
import { getGenerateRecord } from '@/api/wms/generateRecord';
import { addStockIoDetail } from '@/api/wms/stockIoDetail';
// 通用防抖高阶函数(可放在工具类中全局复用)
function debounce(fn, delay = 300) {
let debounceTimer = null; // 闭包保存定时器,避免重复创建
// 返回被包装后的函数(支持传递参数给业务函数)
return function (...args) {
// 1. 清除上一次未执行的定时器(核心:避免短时间内重复触发)
if (debounceTimer) {
clearTimeout(debounceTimer);
}
// 2. 延迟执行业务函数this 绑定到调用者(适配 Vue 组件上下文)
debounceTimer = setTimeout(() => {
fn.apply(this, args); // 传递参数+保持this指向Vue组件实例
}, delay);
};
}
export default {
data() {
return {
text: '',
isLoading: false,
isSubmitting: false,
inputHint: '请输入二维码内容,系统将自动解析',
};
},
components: {
ProductInfo,
RawMaterialInfo,
},
created() {
// 包装“输入解析+提交”的业务函数,生成带防抖的版本
this.debouncedHandleInput = debounce(
this.actualParseAndSubmit, // 实际的业务逻辑函数
500 // 防抖延迟时间
);
},
computed: {
result() {
try {
const text = JSON.parse(this.text)
return text;
} catch (error) {
return {};
}
},
// 验证数据是否有效
isValid() {
const requiredFields = ['ioType', 'itemType', 'itemId', 'quantity'];
// 检查是否包含所有必要字段
const hasAllFields = requiredFields.every(field => this.result.hasOwnProperty(field));
// 检查出入库类型是否有效
const isIoTypeValid = this.result.ioType === 'in' || this.result.ioType === 'out';
// 检查物料类型是否有效
const isItemTypeValid = ['product', 'semi', 'raw_material'].includes(this.result.itemType);
// 检查数量是否为有效数字
const isQuantityValid = !isNaN(Number(this.result.quantity)) && Number(this.result.quantity) > 0;
return hasAllFields && isIoTypeValid && isItemTypeValid && isQuantityValid;
}
},
methods: {
handleInput(value) {
const trimmedValue = value.trim();
// 空值场景:立即反馈,不触发防抖逻辑
if (!trimmedValue) {
this.inputHint = "请输入二维码内容,系统将自动解析";
return;
}
// 基础校验不通过:立即反馈,不触发防抖逻辑
if (!this.isValid) {
this.inputHint = "输入内容不符合基础格式要求,请检查";
return;
}
// 触发防抖后的业务逻辑(此时已自动防抖)
this.debouncedHandleInput(trimmedValue);
},
// 5. 实际的“JSON解析+提交”业务逻辑(纯业务,无防抖代码)
async actualParseAndSubmit(validValue) {
this.isLoading = true;
try {
// 1. 解析JSON
const res = await getGenerateRecord(validValue);
const parsedData = JSON.stringify(res.data.content);
this.result = parsedData;
// 3. 解析成功:反馈+提交
this.inputHint = "数据解析成功,可以提交操作";
this.$message.success({
message: "数据解析成功",
duration: 1500,
});
this.handleSubmit(parsedData); // 调用提交接口
} catch (error) {
// 6. 解析失败:错误处理
this.inputHint = "无法解析数据,请确保输入的是有效的二维码内容";
this.$message.error({
message: "解析失败,请检查输入内容",
duration: 2000,
});
} finally {
// 7. 无论成功/失败,结束加载状态
this.isLoading = false;
}
},
handleBlur() {
// 自动重新聚焦,方便连续扫描
this.$nextTick(() => {
this.$refs.textarea.focus();
});
},
handleSubmit() {
if (!this.isValid) return;
this.isSubmitting = true;
addStockIoDetail({
...this.result,
recordType: 1, // recordType为1标识扫码录入
})
.then(res => {
this.$message.success({
message: '操作成功',
duration: 2000
});
})
.catch(err => {
this.$message.error({
message: '操作失败: ' + (err.message || '未知错误'),
duration: 3000
});
})
.finally(() => {
this.isSubmitting = false;
// 提交成功后重置表单,方便下一次操作
this.handleReset();
});
},
handleReset() {
this.text = '';
this.inputHint = '请输入二维码内容,系统将自动解析';
// 重置后重新聚焦
this.$nextTick(() => {
this.$refs.textarea.focus();
});
},
// 格式化物料类型显示文本
formatItemType(type) {
const typeMap = {
'product': '成品',
'semi': '半成品',
'raw_material': '原材料'
};
return typeMap[type] || '未知类型';
}
},
mounted() {
// 确保组件挂载后自动聚焦
this.$nextTick(() => {
this.$refs.textarea.focus();
});
}
}
</script>
<style scoped>
.qr-parser-container {
display: flex;
justify-content: center;
padding: 20px;
background-color: #f5f7fa;
min-height: 100vh;
}
.card {
width: 100%;
background: #fff;
border-radius: 10px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
overflow: hidden;
}
.card-header {
padding: 20px 24px;
border-bottom: 1px solid #e8e8e8;
}
.title {
margin: 0;
font-size: 20px;
color: #1f2329;
font-weight: 600;
}
.subtitle {
margin: 5px 0 0;
font-size: 14px;
color: #6b7280;
}
.input-section {
padding: 24px;
}
.el-input {
transition: all 0.3s ease;
}
.el-input.invalid-input .el-textarea__inner {
border-color: #f56c6c;
box-shadow: 0 0 0 2px rgba(245, 108, 108, 0.2);
}
.input-hint {
margin-top: 8px;
font-size: 12px;
color: #6b7280;
display: flex;
align-items: center;
}
.input-hint i {
margin-right: 4px;
font-size: 14px;
}
.result-section {
padding: 0 24px 24px;
}
.result-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.result-header h3 {
margin: 0;
font-size: 16px;
color: #1f2329;
font-weight: 500;
}
.result-table {
margin-bottom: 24px;
}
.action-buttons {
display: flex;
gap: 12px;
justify-content: flex-end;
}
.loading-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
}
/* 淡入动画 */
.fade-in {
animation: fadeIn 0.5s ease-in-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>