<script lang="ts" setup>
|
import type { CrmSaleQuotationAiApi } from '#/api/crm/saleQuotation/ai';
|
|
import { ref } from 'vue';
|
|
import { IconifyIcon } from '@vben/icons';
|
|
import { Modal, Button, Upload, Descriptions, Table, Tag, message, Alert } from 'ant-design-vue';
|
|
import { ocrSaleQuotation } from '#/api/crm/saleQuotation/ai';
|
import { uploadFile } from '#/api/system/storage';
|
|
defineOptions({ name: 'CrmSaleQuotationOcrUpload' });
|
|
const emit = defineEmits<{
|
success: [data: CrmSaleQuotationAiApi.OcrResultVO];
|
}>();
|
|
const open = ref(false);
|
const step = ref(0); // 0=upload, 1=processing, 2=preview
|
const fileList = ref<any[]>([]);
|
const ocrResult = ref<CrmSaleQuotationAiApi.OcrResultVO>();
|
const processingTip = ref('');
|
|
const itemColumns = [
|
{ title: '物料名称', dataIndex: 'itemName', key: 'itemName' },
|
{ title: '规格型号', dataIndex: 'itemSpec', key: 'itemSpec' },
|
{ title: '数量', dataIndex: 'count', key: 'count' },
|
{ title: '单价', dataIndex: 'quotationPrice', key: 'quotationPrice' },
|
];
|
|
function handleOpen() {
|
open.value = true;
|
step.value = 0;
|
fileList.value = [];
|
ocrResult.value = undefined;
|
}
|
|
/** 自定义上传 */
|
async function handleUpload(options: any) {
|
const { file, onSuccess, onError } = options;
|
try {
|
const result = await uploadFile([file as File]);
|
const blobId = result?.[0]?.id;
|
if (!blobId) {
|
onError(new Error('上传失败'));
|
return;
|
}
|
onSuccess({ blobId }, file);
|
// 开始 OCR 识别
|
step.value = 1;
|
processingTip.value = '正在进行 AI OCR 识别,请稍候...';
|
const ocrData = await ocrSaleQuotation(blobId);
|
// 检查错误
|
if (ocrData.rawText && !ocrData.name && !ocrData.customerName && ocrData.items?.length === 0) {
|
// 所有字段为 null,rawText 包含错误提示
|
processingTip.value = '';
|
step.value = 0;
|
message.error(ocrData.rawText);
|
return;
|
}
|
// 检查是否全部为空
|
const hasData = ocrData.name || ocrData.customerName || ocrData.quotationTime
|
|| ocrData.validUntil || ocrData.taxRate || ocrData.discountPercent
|
|| ocrData.remark || (ocrData.items && ocrData.items.length > 0);
|
if (!hasData) {
|
processingTip.value = '';
|
step.value = 0;
|
message.warning('未识别到报价数据,请手动录入或重新上传');
|
return;
|
}
|
ocrResult.value = ocrData;
|
step.value = 2;
|
} catch {
|
processingTip.value = '';
|
step.value = 0;
|
onError(new Error('上传失败'));
|
}
|
}
|
|
function handleConfirm() {
|
if (ocrResult.value) {
|
emit('success', ocrResult.value);
|
}
|
handleClose();
|
}
|
|
function handleReset() {
|
step.value = 0;
|
fileList.value = [];
|
ocrResult.value = undefined;
|
processingTip.value = '';
|
}
|
|
function handleClose() {
|
open.value = false;
|
handleReset();
|
}
|
|
defineExpose({ open: handleOpen });
|
</script>
|
|
<template>
|
<Modal v-model:open="open" title="AI 识别录入报价单" width="680px" :footer="null" @cancel="handleClose">
|
<!-- Step 0: 上传 -->
|
<template v-if="step === 0">
|
<Upload.Dragger
|
v-model:file-list="fileList"
|
:max-count="1"
|
:custom-request="handleUpload"
|
accept=".pdf,.doc,.docx,.xls,.xlsx,.png,.jpg,.jpeg,.gif,.bmp,.txt,.csv"
|
@remove="handleReset"
|
>
|
<p class="text-4xl text-gray-400">
|
<IconifyIcon icon="ant-design:inbox-outlined" />
|
</p>
|
<p class="text-base text-gray-500">点击或拖拽报价文件到此区域上传</p>
|
<p class="text-sm text-gray-400">
|
支持 PDF、Word、Excel、图片、纯文本文件
|
</p>
|
</Upload.Dragger>
|
</template>
|
|
<!-- Step 1: 识别中 -->
|
<template v-else-if="step === 1">
|
<div class="flex flex-col items-center gap-4 py-12">
|
<a-spin size="large" />
|
<span class="text-base text-gray-500">{{ processingTip }}</span>
|
</div>
|
</template>
|
|
<!-- Step 2: 预览结果 -->
|
<template v-else-if="step === 2 && ocrResult">
|
<div class="mb-4">
|
<Descriptions :column="2" bordered size="small">
|
<Descriptions.Item v-if="ocrResult.name" label="报价单名称">
|
{{ ocrResult.name }}
|
</Descriptions.Item>
|
<Descriptions.Item v-if="ocrResult.customerName" label="客户名称(需手动匹配)">
|
<Tag color="orange">{{ ocrResult.customerName }}</Tag>
|
</Descriptions.Item>
|
<Descriptions.Item v-if="ocrResult.quotationTime" label="报价日期">
|
{{ ocrResult.quotationTime }}
|
</Descriptions.Item>
|
<Descriptions.Item v-if="ocrResult.validUntil" label="有效期至">
|
{{ ocrResult.validUntil }}
|
</Descriptions.Item>
|
<Descriptions.Item v-if="ocrResult.taxRate != null" label="税率(%)">
|
{{ ocrResult.taxRate }}
|
</Descriptions.Item>
|
<Descriptions.Item v-if="ocrResult.discountPercent != null" label="折扣率(%)">
|
{{ ocrResult.discountPercent }}
|
</Descriptions.Item>
|
<Descriptions.Item v-if="ocrResult.remark" label="备注" :span="2">
|
{{ ocrResult.remark }}
|
</Descriptions.Item>
|
</Descriptions>
|
</div>
|
|
<!-- 物料明细 -->
|
<div v-if="ocrResult.items?.length" class="mb-4">
|
<div class="mb-2 text-sm font-medium">物料明细(需手动匹配实际物料)</div>
|
<Table
|
:columns="itemColumns"
|
:data-source="ocrResult.items"
|
:pagination="false"
|
bordered
|
size="small"
|
/>
|
</div>
|
|
<Alert
|
message="客户名称和物料名称仅为文本识别结果,需在表单中手动选择匹配的 CRM 客户和实际物料。"
|
type="info"
|
show-icon
|
class="mb-4"
|
/>
|
|
<div class="flex justify-end gap-2">
|
<Button @click="handleReset">重新上传</Button>
|
<Button type="primary" @click="handleConfirm">确认并填入表单</Button>
|
</div>
|
</template>
|
</Modal>
|
</template>
|