Files
klp-oa/klp-ui/src/views/wms/purchasePlan/panels/qualityCerticate.vue

404 lines
13 KiB
Vue
Raw Normal View History

2025-08-02 13:38:04 +08:00
<template>
<div v-loading="loading" :element-loading-text="loadingText">
<el-steps :active="1" align-center simple>
<el-step style="cursor: pointer;" @click.native="active = 0" title="上传质保单" icon="el-icon-upload" />
<el-step style="cursor: pointer;" @click.native="active = 1" title="质保单处理" icon="el-icon-edit" />
<el-step style="cursor: pointer;" @click.native="active = 2" title="质保单审核" icon="el-icon-check" />
</el-steps>
2025-08-04 11:15:42 +08:00
<div v-if="active === 0" style="padding: 30px;">
<file-upload v-model="uploadQualityCertificateForm.qualityCertificate" />
</div>
<div v-if="active === 1">
<el-row>
<el-alert title="质保单处理" type="info" />
</el-row>
<!-- 提取质保单信息,选择使用ocr还是使用大模型 -->
<el-row :gutter="20">
<el-col :span="12">
<div
style="border: 1px solid #ccc; border-radius: 10px; padding: 20px; cursor: pointer; text-align: center; position: relative;">
<!-- 齿轮图标绝对定位在左上角 -->
<i class="el-icon-setting"
style="position: absolute; left: 10px; top: 10px; font-size: 20px; color: #606266; cursor: pointer; z-index: 2;"
@click.stop="openKeyListDialog"></i>
<div style="margin-bottom: 10px; display: flex; align-items: center; justify-content: center;">
<span style="font-weight: bold; font-size: 16px;">OCR识别</span>
<el-tooltip content="通过OCR自动识别PDF质保单内容" placement="top">
<i class="el-icon-question" style="margin-left: 6px; color: #409EFF; cursor: pointer;"></i>
</el-tooltip>
</div>
<img @click="handleOcr" :src="ocrImage" alt="ocr" style="width: 120px; height: 120px; object-fit: contain;"
append-to-body />
</div>
</el-col>
<el-col :span="12">
<div style="border: 1px solid #ccc; border-radius: 10px; padding: 20px; cursor: pointer; text-align: center;">
<div style="margin-bottom: 10px; display: flex; align-items: center; justify-content: center;">
<span style="font-weight: bold; font-size: 16px;">大模型识别</span>
<el-tooltip content="通过AI大模型识别图片质保单内容" placement="top">
<i class="el-icon-question" style="margin-left: 6px; color: #409EFF; cursor: pointer;"></i>
</el-tooltip>
</div>
<img @click="handleModel" :src="modelImage" alt="model"
style="width: 120px; height: 120px; object-fit: contain;" />
</div>
</el-col>
</el-row>
</div>
2025-08-02 13:38:04 +08:00
2025-08-04 11:15:42 +08:00
<div v-if="active === 2">
<div v-if="resultDiff">
<merger :info="info" :old-result="oldResult" :new-result="newResult" @confirm="handleMergerConfirm" />
</div>
<div v-else>
2025-08-02 13:38:04 +08:00
<el-row>
2025-08-04 11:15:42 +08:00
<el-alert title="请核对识别结果是否正确" type="success" />
2025-08-02 13:38:04 +08:00
</el-row>
<el-row :gutter="20">
<el-col :span="12">
2025-08-04 11:15:42 +08:00
<el-table :data="newResult" style="width: 100%">
<el-table-column prop="attrKey" label="属性名称" />
<el-table-column prop="attrValue" label="属性值" />
</el-table>
</el-col>
2025-08-04 11:15:42 +08:00
<el-col :span="12">
<div>
<img style="width: 100%; height: 100%;" :src="file.url" alt="">
2025-08-02 13:38:04 +08:00
</div>
</el-col>
</el-row>
2025-08-04 11:15:42 +08:00
<el-button type="primary" @click="handleConfirm">确认</el-button>
2025-08-02 13:38:04 +08:00
</div>
2025-08-04 11:15:42 +08:00
</div>
2025-08-02 13:38:04 +08:00
2025-08-04 11:15:42 +08:00
<!-- keyList配置弹窗 -->
<el-dialog title="配置OCR识别字段" :visible.sync="showKeyListDialog" width="400px" append-to-body>
<div>
<el-tag type="info" style="margin-bottom: 8px;">每行一个字段名顺序影响识别</el-tag>
<el-input type="textarea" :rows="8" v-model="keyListInput" placeholder="如:订货单位\n合同号\n产品名称..." />
2025-08-02 13:38:04 +08:00
</div>
2025-08-04 11:15:42 +08:00
<span slot="footer" class="dialog-footer">
<el-button @click="showKeyListDialog = false">取消</el-button>
<el-button type="primary" @click="saveKeyList">保存</el-button>
</span>
</el-dialog>
2025-08-02 13:38:04 +08:00
</div>
</template>
<script>
import FileUpload from '@/components/FileUpload'
import { listByIds } from '@/api/system/oss'
import { updatePurchasePlanDetail } from '@/api/wms/purchasePlanDetail'
2025-08-04 11:15:42 +08:00
import { recognizeText, recognizeBomByModel, recognizePdfText } from '@/api/system/ocr'
2025-08-02 16:34:08 +08:00
import { listBomItem } from '@/api/wms/bomItem'
import { getRawMaterial } from '@/api/wms/rawMaterial'
import Merger from './merger.vue'
import modelImage from '@/assets/images/model.png'
import ocrImage from '@/assets/images/ocr.png'
2025-08-02 13:38:04 +08:00
const so = {
annex: {
loading: '正在保存质保单',
2025-08-02 16:13:13 +08:00
handler: async (vm, newVal) => {
return await updatePurchasePlanDetail({
2025-08-02 13:38:04 +08:00
...vm.info,
annex: newVal
})
}
},
ocr: {
loading: '等待ocr识别结果',
2025-08-02 16:13:13 +08:00
handler: async (vm) => {
2025-08-04 11:15:42 +08:00
function extractData(text) {
// 优先读取localStorage配置的keyList
let keyList = [];
try {
const local = localStorage.getItem('ocrKeyList');
if (local) {
keyList = local.split('\n').map(k => k.trim()).filter(k => k);
}
} catch { }
// 默认keyList
if (!keyList.length) {
keyList = [
"订货单位", "合同号", "产品名称", "执行标准",
"卷号", "原料坯号", "规格", "材质",
"表面状态", "调制度", "切边要求", "包装种类",
"毛重", "净重", "参考长度"
];
}
// 预处理:将复合键合并(如"合同号"、"卷号"等)
let normalizedText = text
.replace(/卷\s*号/g, "卷号")
.replace(/原料\s*坯号/g, "原料坯号")
.replace(/切边\s*要求/g, "切边要求")
.replace(/包装\s*种类/g, "包装种类");
const lines = normalizedText.split(/\r?\n/).filter(line => line.trim());
const result = [];
// 核心数据行处理(跳过首行公司名)
for (let i = 1; i < lines.length; i++) {
let currentLine = lines[i];
let remaining = currentLine;
// 处理单行中的多组键值对
while (remaining) {
let found = false;
// 检查剩余文本是否以已知键开头
for (const key of keyList) {
if (remaining.startsWith(key)) {
found = true;
// 移动到键之后的位置
let afterKey = remaining.substring(key.length).trim();
// 查找下一个键的起始位置
let nextKeyIndex = afterKey.length;
for (const k of keyList) {
const idx = afterKey.indexOf(k);
if (idx > -1 && idx < nextKeyIndex) {
nextKeyIndex = idx;
}
}
// 提取当前键的值
const value = afterKey.substring(0, nextKeyIndex).trim();
result.push({
attrKey: key,
attrValue: key === "毛重" || key === "净重"
? value.replace(/\D/g, "") // 提取数字部分
: value
});
// 更新剩余文本
remaining = afterKey.substring(nextKeyIndex).trim();
break;
}
}
// 如果没有找到更多键,结束循环
if (!found) break;
}
}
return result;
}
const { text } = (await recognizePdfText({ pdfUrl: vm.file.url })).data
console.log(text)
const result = extractData(text)
console.log(result)
vm.newResult = result;
return result;
2025-08-02 13:38:04 +08:00
}
},
model: {
loading: '等待大模型识别结果',
2025-08-02 16:13:13 +08:00
handler: async (vm) => {
const res = await recognizeBomByModel({ imageUrl: vm.file.url })
2025-08-02 16:39:40 +08:00
vm.newResult = res.data.attributes;
vm.$modal.msgSuccess("识别成功");
2025-08-02 16:13:13 +08:00
return res;
}
2025-08-02 13:38:04 +08:00
},
bom: {
loading: '正在处理BOM'
},
oss: {
loading: '正在获取质保单',
2025-08-02 16:13:13 +08:00
handler: async (vm, newVal) => {
const res = await listByIds(newVal)
vm.file = res.data[0];
vm.active = 1;
return res.data[0];
2025-08-02 13:38:04 +08:00
}
},
old: {
loading: '正在获取历史质保单',
2025-08-02 16:13:13 +08:00
handler: async (vm, newVal) => {
// 查询对应的bomId
const res = await getRawMaterial(vm.info.rawMaterialId)
const bomId = res.data.bomId;
const bomItemRes = await listBomItem({
bomId,
2025-08-02 13:38:04 +08:00
})
2025-08-02 16:13:13 +08:00
vm.oldResult = bomItemRes.rows;
return bomItemRes.rows;
2025-08-02 13:38:04 +08:00
}
2025-08-04 09:54:01 +08:00
},
compare: {
loading: '正在比较新旧质保单',
handler: async (vm) => {
// 先检查新旧result是否一致
if (vm.oldResult.length !== vm.newResult.length) {
vm.resultDiff = true;
return;
}
// 比较新旧result是否一致
for (let i = 0; i < vm.oldResult.length; i++) {
if (vm.oldResult[i].attrKey !== vm.newResult[i].attrKey || vm.oldResult[i].attrValue !== vm.newResult[i].attrValue) {
vm.resultDiff = true;
return;
}
}
vm.resultDiff = false;
return false;
}
2025-08-02 13:38:04 +08:00
}
}
2025-08-04 11:15:42 +08:00
// 原子操作, 用于细化的进度展示, 無論其同步還是異步一律視作異步函數執行, 並返回一個Promise
2025-08-02 16:34:08 +08:00
const atoms = {
}
2025-08-02 13:38:04 +08:00
export default {
name: 'QualityCerticate',
components: {
2025-08-02 16:58:34 +08:00
FileUpload,
Merger
2025-08-02 13:38:04 +08:00
},
props: {
info: {
type: Object,
default: () => ({})
}
},
watch: {
info: {
handler(newVal) {
this.active = 0;
if (newVal.annex) {
this.uploadQualityCertificateForm.qualityCertificate = newVal.annex;
} else {
this.uploadQualityCertificateForm.qualityCertificate = undefined;
}
},
deep: true,
immediate: true
},
'uploadQualityCertificateForm.qualityCertificate': {
handler(newVal) {
if (newVal) {
2025-08-04 11:15:42 +08:00
this.loadingMethod('oss')
2025-08-02 13:38:04 +08:00
}
this.loadingMethod('annex')
},
immediate: true
}
},
data() {
return {
uploadQualityCertificateForm: {
qualityCertificate: undefined,
qualityCertificateType: undefined,
},
active: 0,
file: undefined,
loading: false,
loadingText: '加载中...',
2025-08-02 16:13:13 +08:00
resultDiff: true,
oldResult: [],
newResult: [],
2025-08-02 16:34:08 +08:00
modelImage,
ocrImage,
2025-08-04 11:15:42 +08:00
showKeyListDialog: false,
keyListInput: '',
2025-08-02 13:38:04 +08:00
}
},
methods: {
handleOcr() {
2025-08-04 11:15:42 +08:00
// 识别file是不是pdf, 只有pdf可以使用ocr
if (!this.file.url.endsWith('.pdf')) {
this.$modal.msgError('质保单不是pdf格式');
return;
}
2025-08-04 09:54:01 +08:00
this.loadingMethod('ocr', async (res) => {
await this.loadingMethod('old')
await this.loadingMethod('compare')
this.active = 2;
2025-08-02 13:38:04 +08:00
})
},
handleModel() {
2025-08-02 16:13:13 +08:00
this.loadingMethod('model', async (res) => {
await this.loadingMethod('old')
2025-08-04 09:54:01 +08:00
await this.loadingMethod('compare')
2025-08-02 16:13:13 +08:00
this.active = 2;
})
2025-08-04 11:15:42 +08:00
2025-08-02 13:38:04 +08:00
},
async handleConfirm() {
// 变更状态
this.active = 3;
this.$emit('confirm')
},
2025-08-02 16:34:08 +08:00
handleMergerConfirm(res) {
if (res.status === 'start') {
this.loading = true;
this.loadingText = '正在处理产品BOM';
} else if (res.status === 'success') {
this.loading = false;
this.active = 3;
this.$emit('confirm')
} else if (res.status === 'error') {
this.loading = false;
this.$modal.msgError('质保单处理失败');
}
},
2025-08-02 16:13:13 +08:00
async loadingMethod(key, fn) {
2025-08-02 13:38:04 +08:00
this.loading = true;
this.loadingText = so[key].loading;
2025-08-02 16:13:13 +08:00
try {
const res = await so[key].handler(this, this.uploadQualityCertificateForm.qualityCertificate)
fn && await fn(res)
return res;
} catch {
2025-08-02 13:38:04 +08:00
this.$modal.msgError('操作失败');
2025-08-02 16:13:13 +08:00
} finally {
this.loading = false;
}
},
// 比较新旧result是否一致
handleCompareResult() {
// 先检查新旧result是否一致
if (this.oldResult.length !== this.newResult.length) {
this.resultDiff = true;
return;
}
// 比较新旧result是否一致
for (let i = 0; i < this.oldResult.length; i++) {
if (this.oldResult[i].attrKey !== this.newResult[i].attrKey || this.oldResult[i].attrValue !== this.newResult[i].attrValue) {
this.resultDiff = true;
return;
}
}
this.resultDiff = false;
2025-08-04 11:15:42 +08:00
},
// 保存keyList到localStorage
saveKeyList() {
localStorage.setItem('ocrKeyList', this.keyListInput)
this.showKeyListDialog = false
this.$message.success('已保存OCR字段配置')
},
// 打开弹窗时初始化内容
openKeyListDialog() {
let local = ''
try {
local = localStorage.getItem('ocrKeyList') || ''
} catch { }
if (local) {
this.keyListInput = local
} else {
this.keyListInput = [
"订货单位", "合同号", "产品名称", "执行标准",
"卷号", "原料坯号", "规格", "材质",
"表面状态", "调制度", "切边要求", "包装种类",
"毛重", "净重", "参考长度"
].join('\n')
}
this.showKeyListDialog = true
},
2025-08-02 13:38:04 +08:00
}
}
</script>
<style scoped>
.el-row {
margin-bottom: 20px;
margin-top: 20px;
}
</style>