2025-08-23 10:35:43 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="qr-parser-container">
|
2025-08-23 17:22:06 +08:00
|
|
|
|
|
2025-08-23 10:35:43 +08:00
|
|
|
|
<div class="card">
|
|
|
|
|
|
<el-alert title="请将当前输入法切换到英文输入后再进行扫码操作" type="warning"></el-alert>
|
|
|
|
|
|
<div class="card-header">
|
|
|
|
|
|
<h2 class="title">二维码解析器</h2>
|
|
|
|
|
|
<p class="subtitle">扫描或输入二维码内容进行解析</p>
|
|
|
|
|
|
</div>
|
2025-08-23 17:22:06 +08:00
|
|
|
|
|
2025-08-23 10:35:43 +08:00
|
|
|
|
<div class="input-section">
|
2025-08-23 17:22:06 +08:00
|
|
|
|
<el-input v-model="text" @input="handleInput" @blur="handleBlur" ref="textarea"
|
|
|
|
|
|
placeholder="请扫描二维码或粘贴二维码文本内容..." :rows="3" type="textarea"
|
|
|
|
|
|
:class="{ 'invalid-input': !isValid && text.length > 0 }" />
|
2025-08-23 10:35:43 +08:00
|
|
|
|
<div class="input-hint">
|
|
|
|
|
|
<i class="el-icon-info-circle"></i>
|
|
|
|
|
|
<span>{{ inputHint }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-08-23 17:22:06 +08:00
|
|
|
|
|
2025-08-23 10:35:43 +08:00
|
|
|
|
<!-- 加载状态 -->
|
2025-08-23 17:22:06 +08:00
|
|
|
|
<el-loading v-if="isLoading" text="正在解析数据..." class="loading-overlay"></el-loading>
|
|
|
|
|
|
|
2025-08-23 10:35:43 +08:00
|
|
|
|
<!-- 二维码解析结果 - 只有数据有效时才显示 -->
|
|
|
|
|
|
<div v-if="isValid" class="result-section fade-in">
|
|
|
|
|
|
<div class="result-header">
|
|
|
|
|
|
<h3>解析结果</h3>
|
2025-08-23 17:22:06 +08:00
|
|
|
|
<el-tag :type="result.ioType === 'in' ? 'success' : 'warning'" size="small">
|
2025-08-23 10:35:43 +08:00
|
|
|
|
{{ result.ioType === 'in' ? '入库' : '出库' }}
|
|
|
|
|
|
</el-tag>
|
|
|
|
|
|
</div>
|
2025-08-23 17:22:06 +08:00
|
|
|
|
|
2025-08-23 10:35:43 +08:00
|
|
|
|
<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">
|
2025-08-23 17:22:06 +08:00
|
|
|
|
<ProductInfo v-if="result.itemType === 'product' || result.itemType === 'semi'"
|
|
|
|
|
|
:productId="result.itemId" />
|
2025-08-23 10:35:43 +08:00
|
|
|
|
<RawMaterialInfo v-else-if="result.itemType === 'raw_material'" :materialId="result.itemId" />
|
2025-08-23 17:22:06 +08:00
|
|
|
|
<el-input v-else disabled v-model="result.itemId" placeholder="未知物料" :disabled="true"
|
|
|
|
|
|
style="width: 100%;" />
|
2025-08-23 10:35:43 +08:00
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="数量" :span="1">
|
|
|
|
|
|
{{ result.quantity }}
|
|
|
|
|
|
</el-descriptions-item>
|
|
|
|
|
|
</el-descriptions>
|
2025-08-23 17:22:06 +08:00
|
|
|
|
|
2025-08-23 10:35:43 +08:00
|
|
|
|
<div class="action-buttons">
|
2025-08-23 17:22:06 +08:00
|
|
|
|
<el-button type="primary" @click="handleSubmit" :loading="isSubmitting" icon="el-icon-check">
|
2025-08-23 10:35:43 +08:00
|
|
|
|
确认执行
|
|
|
|
|
|
</el-button>
|
2025-08-23 17:22:06 +08:00
|
|
|
|
<el-button type="default" @click="handleReset" icon="el-icon-refresh-right">
|
2025-08-23 10:35:43 +08:00
|
|
|
|
重置
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
import ProductInfo from '@/components/KLPService/Renderer/ProductInfo';
|
|
|
|
|
|
import RawMaterialInfo from '@/components/KLPService/Renderer/RawMaterialInfo';
|
2025-09-12 17:07:57 +08:00
|
|
|
|
import { getGenerateRecord } from '@/api/wms/generateRecord';
|
2025-08-23 10:35:43 +08:00
|
|
|
|
import { addStockIoDetail } from '@/api/wms/stockIoDetail';
|
|
|
|
|
|
|
2025-08-23 17:22:06 +08:00
|
|
|
|
// 通用防抖高阶函数(可放在工具类中全局复用)
|
|
|
|
|
|
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);
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-23 10:35:43 +08:00
|
|
|
|
export default {
|
|
|
|
|
|
data() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
text: '',
|
|
|
|
|
|
isLoading: false,
|
|
|
|
|
|
isSubmitting: false,
|
|
|
|
|
|
inputHint: '请输入二维码内容,系统将自动解析',
|
|
|
|
|
|
};
|
|
|
|
|
|
},
|
|
|
|
|
|
components: {
|
|
|
|
|
|
ProductInfo,
|
|
|
|
|
|
RawMaterialInfo,
|
|
|
|
|
|
},
|
2025-08-23 17:22:06 +08:00
|
|
|
|
created() {
|
|
|
|
|
|
// 包装“输入解析+提交”的业务函数,生成带防抖的版本
|
|
|
|
|
|
this.debouncedHandleInput = debounce(
|
|
|
|
|
|
this.actualParseAndSubmit, // 实际的业务逻辑函数
|
|
|
|
|
|
500 // 防抖延迟时间
|
|
|
|
|
|
);
|
|
|
|
|
|
},
|
2025-08-23 10:35:43 +08:00
|
|
|
|
computed: {
|
|
|
|
|
|
result() {
|
|
|
|
|
|
try {
|
2025-08-23 17:22:06 +08:00
|
|
|
|
const text = JSON.parse(this.text)
|
|
|
|
|
|
return text;
|
2025-08-23 10:35:43 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
return {};
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
// 验证数据是否有效
|
|
|
|
|
|
isValid() {
|
2025-08-23 17:22:06 +08:00
|
|
|
|
const requiredFields = ['ioType', 'itemType', 'itemId', 'quantity'];
|
2025-08-23 10:35:43 +08:00
|
|
|
|
// 检查是否包含所有必要字段
|
|
|
|
|
|
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;
|
2025-08-23 17:22:06 +08:00
|
|
|
|
|
2025-08-23 10:35:43 +08:00
|
|
|
|
return hasAllFields && isIoTypeValid && isItemTypeValid && isQuantityValid;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
methods: {
|
2025-08-23 17:22:06 +08:00
|
|
|
|
handleInput(value) {
|
|
|
|
|
|
const trimmedValue = value.trim();
|
|
|
|
|
|
|
|
|
|
|
|
// 空值场景:立即反馈,不触发防抖逻辑
|
|
|
|
|
|
if (!trimmedValue) {
|
|
|
|
|
|
this.inputHint = "请输入二维码内容,系统将自动解析";
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 基础校验不通过:立即反馈,不触发防抖逻辑
|
|
|
|
|
|
if (!this.isValid) {
|
|
|
|
|
|
this.inputHint = "输入内容不符合基础格式要求,请检查";
|
2025-08-23 10:35:43 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-08-23 17:22:06 +08:00
|
|
|
|
|
|
|
|
|
|
// 触发防抖后的业务逻辑(此时已自动防抖)
|
|
|
|
|
|
this.debouncedHandleInput(trimmedValue);
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 5. 实际的“JSON解析+提交”业务逻辑(纯业务,无防抖代码)
|
2025-09-12 17:07:57 +08:00
|
|
|
|
async actualParseAndSubmit(validValue) {
|
2025-08-23 10:35:43 +08:00
|
|
|
|
this.isLoading = true;
|
2025-08-23 17:22:06 +08:00
|
|
|
|
try {
|
|
|
|
|
|
// 1. 解析JSON
|
2025-09-12 17:07:57 +08:00
|
|
|
|
const res = await getGenerateRecord(validValue);
|
|
|
|
|
|
const parsedData = JSON.stringify(res.data.content);
|
2025-08-23 17:22:06 +08:00
|
|
|
|
|
2025-09-12 17:07:57 +08:00
|
|
|
|
this.result = parsedData;
|
2025-08-23 17:22:06 +08:00
|
|
|
|
// 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;
|
|
|
|
|
|
}
|
2025-08-23 10:35:43 +08:00
|
|
|
|
},
|
2025-08-23 17:22:06 +08:00
|
|
|
|
|
2025-08-23 10:35:43 +08:00
|
|
|
|
handleBlur() {
|
|
|
|
|
|
// 自动重新聚焦,方便连续扫描
|
|
|
|
|
|
this.$nextTick(() => {
|
|
|
|
|
|
this.$refs.textarea.focus();
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
handleSubmit() {
|
|
|
|
|
|
if (!this.isValid) return;
|
2025-08-23 17:22:06 +08:00
|
|
|
|
|
2025-08-23 10:35:43 +08:00
|
|
|
|
this.isSubmitting = true;
|
2025-08-23 17:22:06 +08:00
|
|
|
|
|
2025-09-12 17:07:57 +08:00
|
|
|
|
addStockIoDetail({
|
|
|
|
|
|
...this.result,
|
|
|
|
|
|
recordType: 1, // recordType为1标识扫码录入
|
|
|
|
|
|
})
|
2025-08-23 10:35:43 +08:00
|
|
|
|
.then(res => {
|
|
|
|
|
|
this.$message.success({
|
|
|
|
|
|
message: '操作成功',
|
|
|
|
|
|
duration: 2000
|
|
|
|
|
|
});
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(err => {
|
|
|
|
|
|
this.$message.error({
|
|
|
|
|
|
message: '操作失败: ' + (err.message || '未知错误'),
|
|
|
|
|
|
duration: 3000
|
|
|
|
|
|
});
|
|
|
|
|
|
})
|
|
|
|
|
|
.finally(() => {
|
|
|
|
|
|
this.isSubmitting = false;
|
2025-08-23 11:55:45 +08:00
|
|
|
|
// 提交成功后重置表单,方便下一次操作
|
|
|
|
|
|
this.handleReset();
|
2025-08-23 10:35:43 +08:00
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
2025-08-23 17:22:06 +08:00
|
|
|
|
|
2025-08-23 10:35:43 +08:00
|
|
|
|
to {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
transform: translateY(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|