| src/api/crm/saleQuotation/ai/index.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/api/erp/purchase/invoice/ai/index.ts | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/crm/saleQuotation/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/crm/saleQuotation/modules/form.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/crm/saleQuotation/modules/ocr-upload.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/erp/purchase/invoice/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/erp/purchase/invoice/modules/form.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/erp/purchase/invoice/modules/ocr-upload.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
src/api/crm/saleQuotation/ai/index.ts
@@ -9,7 +9,7 @@ quotationPrice?: number; } /** AI OCR è¯å«æ¥ä»·æä»¶ååº */ /** è¯å«æ¥ä»·æä»¶ååº */ export interface OcrResultVO { name?: string; customerName?: string; @@ -23,7 +23,7 @@ } } /** AI OCR è¯å«æ¥ä»·æä»¶ */ /** è¯å«æ¥ä»·æä»¶ */ export function ocrSaleQuotation(blobId: number) { return requestClient.post<CrmSaleQuotationAiApi.OcrResultVO>( '/crm/sale-quotation/ai/ocr', src/api/erp/purchase/invoice/ai/index.ts
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,33 @@ import { requestClient } from '#/api/request'; export namespace ErpPurchaseInvoiceAiApi { /** è¯å«å票ååº */ export interface OcrResultVO { /** å票å·ç */ invoiceNo?: string; /** å票æ¬å¤´ï¼è´ä¹°æ¹åç§°ï¼ */ invoiceTitle?: string; /** é宿¹åç§°ï¼ææ¬ï¼ä¾æ ¸å¯¹ä¾åºåï¼ */ supplierName?: string; /** æ¥ç¥¨éé¢ï¼ä»·ç¨å计ï¼ï¼åä½ï¼å */ price?: number; /** å¼ç¥¨æ¥æï¼æ ¼å¼ yyyy-MM-dd */ invoiceTime?: string; /** å票类å */ invoiceType?: string; /** ç¨çï¼ç¾åæ¯ï¼ */ taxRate?: number; /** 夿³¨ */ remark?: string; /** åå§è¯å«ææ¬ï¼è¯å«å¤±è´¥æ¶ä¸ºé误æç¤ºï¼ */ rawText?: string; } } /** è¯å«å票æä»¶ */ export function ocrPurchaseInvoice(blobId: number) { return requestClient.post<ErpPurchaseInvoiceAiApi.OcrResultVO>( '/erp/purchase-invoice/ai/ocr', { blobId }, ); } src/views/crm/saleQuotation/index.vue
@@ -43,7 +43,7 @@ formModalApi.setData(null).open(); } /** AI è¯å«å½å ¥ */ /** è¯å«å½å ¥ */ function handleOcrCreate() { ocrUploadRef.value?.open(); } @@ -159,7 +159,7 @@ onClick: handleCreate, }, { label: 'AI è¯å«å½å ¥', label: 'è¯å«å½å ¥', type: 'default', auth: ['crm:sale-quotation:create'], onClick: handleOcrCreate, src/views/crm/saleQuotation/modules/form.vue
@@ -155,7 +155,7 @@ type="info" show-icon class="mx-4 mb-4" message="AI è¯å«å°å®¢æ·åç§°ï¼è¯·å¨å®¢æ·éæ©å¨ä¸æå¨å¹é CRM 客æ·" message="è¯å«å°å®¢æ·åç§°ï¼è¯·å¨å®¢æ·éæ©å¨ä¸æå¨å¹é CRM 客æ·" :description="`è¯å«ç»æï¼${ocrCustomerName}`" /> <Form class="mx-4"> src/views/crm/saleQuotation/modules/ocr-upload.vue
@@ -49,7 +49,7 @@ onSuccess({ blobId }, file); // å¼å§ OCR è¯å« step.value = 1; processingTip.value = 'æ£å¨è¿è¡ AI OCR è¯å«ï¼è¯·ç¨å...'; processingTip.value = 'æ£å¨è¿è¡æ¥ä»·åè¯å«ï¼è¯·ç¨å...'; const ocrData = await ocrSaleQuotation(blobId); // æ£æ¥é误 if (ocrData.rawText && !ocrData.name && !ocrData.customerName && ocrData.items?.length === 0) { @@ -101,7 +101,7 @@ </script> <template> <Modal v-model:open="open" title="AI è¯å«å½å ¥æ¥ä»·å" width="680px" :footer="null" @cancel="handleClose"> <Modal v-model:open="open" title="è¯å«å½å ¥æ¥ä»·å" width="680px" :footer="null" @cancel="handleClose"> <!-- Step 0: ä¸ä¼ --> <template v-if="step === 0"> <Upload.Dragger src/views/erp/purchase/invoice/index.vue
@@ -1,11 +1,13 @@ <script lang="ts" setup> import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { ErpPurchaseInvoiceApi } from '#/api/erp/purchase/invoice'; import type { ErpPurchaseInvoiceAiApi } from '#/api/erp/purchase/invoice/ai'; import { Page, useVbenModal } from '@vben/common-ui'; import { downloadFileFromBlobPart } from '@vben/utils'; import { Button, message, Tag } from 'ant-design-vue'; import { nextTick, ref } from 'vue'; import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; import { @@ -17,6 +19,7 @@ import { useGridColumns, useGridFormSchema } from './data'; import Form from './modules/form.vue'; import OcrUpload from './modules/ocr-upload.vue'; defineOptions({ name: 'ErpPurchaseInvoice' }); @@ -24,6 +27,8 @@ connectedComponent: Form, destroyOnClose: true, }); const ocrUploadRef = ref<InstanceType<typeof OcrUpload>>(); /** å·æ°è¡¨æ ¼ */ function handleRefresh() { @@ -40,6 +45,18 @@ /** å建æ¥ç¥¨ */ function handleCreate() { formModalApi.setData({ formType: 'create' }).open(); } /** è¯å«å½å ¥ */ function handleOcrCreate() { ocrUploadRef.value?.open(); } /** OCR è¯å«ç»æç¡®è®¤ */ function handleOcrSuccess(data: ErpPurchaseInvoiceAiApi.OcrResultVO) { nextTick(() => { formModalApi.setData({ formType: 'create', ocrData: data } as any).open(); }); } /** ç¼è¾æ¥ç¥¨ */ @@ -101,6 +118,7 @@ <template> <Page auto-content-height> <FormModal @success="handleRefresh" /> <OcrUpload ref="ocrUploadRef" @success="handleOcrSuccess" /> <Grid table-title="æ¥ç¥¨å表"> <template #toolbar-tools> <TableAction @@ -113,6 +131,12 @@ onClick: handleCreate, }, { label: 'è¯å«å½å ¥', type: 'primary', auth: ['erp:purchase-invoice:create'], onClick: handleOcrCreate, }, { label: $t('ui.actionTitle.export'), type: 'primary', icon: ACTION_ICON.DOWNLOAD, src/views/erp/purchase/invoice/modules/form.vue
@@ -2,12 +2,13 @@ import type { FormType } from '../data'; import type { ErpPurchaseInvoiceApi } from '#/api/erp/purchase/invoice'; import type { ErpPurchaseInvoiceAiApi } from '#/api/erp/purchase/invoice/ai'; import { computed, ref } from 'vue'; import { useVbenModal } from '@vben/common-ui'; import { message } from 'ant-design-vue'; import { Alert, message } from 'ant-design-vue'; import { useVbenForm } from '#/adapter/form'; import { @@ -22,6 +23,7 @@ const emit = defineEmits(['success']); const formData = ref<ErpPurchaseInvoiceApi.PurchaseInvoice>(); const formType = ref<FormType>('create'); const ocrSupplierName = ref(''); const getTitle = computed(() => { if (formType.value === 'create') { @@ -71,13 +73,30 @@ async onOpenChange(isOpen: boolean) { if (!isOpen) { formData.value = undefined; ocrSupplierName.value = ''; return; } const data = modalApi.getData<{ formType: FormType; id?: number }>(); const data = modalApi.getData<{ formType: FormType; id?: number; ocrData?: ErpPurchaseInvoiceAiApi.OcrResultVO; }>(); formType.value = data.formType; formApi.setDisabled(formType.value === 'detail'); formApi.updateSchema(useFormSchema()); await formApi.resetForm(); // OCR è¯å«æ°æ®é¢å¡«ï¼æ°å»ºåºæ¯ï¼ç¼è¾/详æ ä¸å¤çï¼ const ocr = data?.ocrData; ocrSupplierName.value = ocr?.supplierName || ''; if (ocr) { await formApi.setValues({ invoiceNo: ocr.invoiceNo, invoiceTitle: ocr.invoiceTitle, price: ocr.price, invoiceTime: ocr.invoiceTime, remark: ocr.remark, }); } if (!data || !data.id) { return; } @@ -105,6 +124,14 @@ <template> <Modal :title="getTitle" class="w-3/5"> <Alert v-if="ocrSupplierName" type="info" show-icon class="mx-4 mb-4" message="è¯å«å°é宿¹åç§°ï¼è¯·éæ©éè´è®¢åå¹¶æ ¸å¯¹è®¢åä¾åºåæ¯å¦ä¸è´" :description="`è¯å«ç»æï¼${ocrSupplierName}`" /> <Form class="mx-4" /> </Modal> </template> src/views/erp/purchase/invoice/modules/ocr-upload.vue
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,186 @@ <script lang="ts" setup> import type { ErpPurchaseInvoiceAiApi } from '#/api/erp/purchase/invoice/ai'; import { ref } from 'vue'; import { IconifyIcon } from '@vben/icons'; import { Alert, Button, Descriptions, Modal, Tag, Upload, message, } from 'ant-design-vue'; import { ocrPurchaseInvoice } from '#/api/erp/purchase/invoice/ai'; import { uploadFile } from '#/api/system/storage'; defineOptions({ name: 'ErpPurchaseInvoiceOcrUpload' }); const emit = defineEmits<{ success: [data: ErpPurchaseInvoiceAiApi.OcrResultVO]; }>(); const open = ref(false); const step = ref(0); // 0=upload, 1=processing, 2=preview const fileList = ref<any[]>([]); const ocrResult = ref<ErpPurchaseInvoiceAiApi.OcrResultVO>(); const processingTip = ref(''); 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 = 'æ£å¨è¿è¡å票è¯å«ï¼è¯·ç¨å...'; const ocrData = await ocrPurchaseInvoice(blobId); // æ£æ¥é误ï¼rawText 为é误æç¤ºï¼ä¸ä¸å¡åæ®µå ¨ç©º const hasData = !!( ocrData.invoiceNo || ocrData.invoiceTitle || ocrData.supplierName || ocrData.price != null || ocrData.invoiceTime || ocrData.invoiceType || ocrData.taxRate != null || ocrData.remark ); if (ocrData.rawText && !hasData) { processingTip.value = ''; step.value = 0; message.error(ocrData.rawText); return; } 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="è¯å«å½å ¥å票" 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,.png,.jpg,.jpeg" @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ãJPGãPNG æ ¼å¼ï¼ä¸æ¥ç¥¨éä»¶ä¸ä¼ æ ¼å¼ä¸è´</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.invoiceNo" label="å票å·ç "> {{ ocrResult.invoiceNo }} </Descriptions.Item> <Descriptions.Item v-if="ocrResult.invoiceType" label="å票类å"> {{ ocrResult.invoiceType }} </Descriptions.Item> <Descriptions.Item v-if="ocrResult.invoiceTitle" label="å票æ¬å¤´"> {{ ocrResult.invoiceTitle }} </Descriptions.Item> <Descriptions.Item v-if="ocrResult.supplierName" label="é宿¹ï¼éæ ¸å¯¹ï¼"> <Tag color="orange">{{ ocrResult.supplierName }}</Tag> </Descriptions.Item> <Descriptions.Item v-if="ocrResult.price != null" label="æ¥ç¥¨éé¢ï¼å ï¼"> {{ ocrResult.price }} </Descriptions.Item> <Descriptions.Item v-if="ocrResult.invoiceTime" label="å¼ç¥¨æ¥æ"> {{ ocrResult.invoiceTime }} </Descriptions.Item> <Descriptions.Item v-if="ocrResult.taxRate != null" label="ç¨ç(%)"> {{ ocrResult.taxRate }} </Descriptions.Item> <Descriptions.Item v-if="ocrResult.remark" label="夿³¨" :span="2"> {{ ocrResult.remark }} </Descriptions.Item> </Descriptions> </div> <Alert message="é宿¹åç§°ä» ä¸ºåç¥¨ææ¬è¯å«ç»æï¼è¯·å¨è¡¨åä¸éæ©éè´è®¢ååæ ¸å¯¹è®¢åä¾åºåä¸é宿¹æ¯å¦ä¸è´ï¼è¯å«æ°æ®å¯æå¨ä¿®æ¹ã" 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>