feat(报销/拨款): OCR失败时保留空条目供手动填写,支持手动新增条目和行内附件上传

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-08 19:49:03 +08:00
parent 40fdd14d13
commit 156602fd59
2 changed files with 93 additions and 4 deletions

View File

@@ -77,12 +77,14 @@
<!-- 发票明细条目表 -->
<div class="block-title">
拨款明细
<span class="block-title-hint">上传发票后自动识别删除条目将同步移除对应文件</span>
<span class="block-title-hint">上传发票后自动识别识别失败或无发票时可手动添加</span>
<el-button size="mini" type="primary" plain icon="el-icon-plus" @click="addManualItem" style="margin-left:12px;vertical-align:middle">手动添加条目</el-button>
</div>
<div class="invoice-table" v-if="invoiceItems.length">
<div class="invoice-table-header">
<span class="col-reason">事由说明</span>
<span class="col-amount">金额</span>
<span class="col-file">附件</span>
<span class="col-action"></span>
</div>
<div v-for="(item, idx) in invoiceItems" :key="idx" class="invoice-table-row">
@@ -100,6 +102,16 @@
class="col-amount"
@change="recalcTotal"
/>
<div class="col-file">
<el-tooltip v-if="item.ossId" content="已关联附件" placement="top">
<i class="el-icon-paperclip" style="color:#409eff"></i>
</el-tooltip>
<el-upload v-else :action="uploadFileUrl" :headers="uploadHeaders" :show-file-list="false"
:data="{ isPublic: 1 }" :on-success="(res) => onRowFileSuccess(res, idx)"
accept=".pdf,.jpg,.jpeg,.png" class="row-upload">
<el-button size="mini" type="text" icon="el-icon-paperclip"></el-button>
</el-upload>
</div>
<div class="col-action">
<el-button size="mini" type="danger" plain icon="el-icon-delete" circle @click="removeInvoiceItem(idx)" />
</div>
@@ -258,6 +270,7 @@ import { addAppropriationReq, checkAppropriationOcrHealth, listFlowNode, listFlo
import { getEmployeeByUserId } from '@/api/hrm/employee';
import { ccFlowTask } from '@/api/hrm/flow';
import FileUpload from '@/components/FileUpload';
import { getToken } from '@/utils/auth';
import UserMultiSelect from '@/components/UserSelect/multi.vue';
import UserSelect from '@/components/UserSelect/single.vue';
@@ -280,6 +293,8 @@ export default {
// 发票明细条目
// OCR服务状态null=检测中true=正常false=不可用
ocrAvailable: null,
uploadFileUrl: process.env.VUE_APP_BASE_API + '/system/oss/upload',
uploadHeaders: { Authorization: 'Bearer ' + getToken() },
// 发票明细条目
invoiceItems: [],
// OCR加载状态 { ossId: true/false }
@@ -368,6 +383,7 @@ export default {
async triggerOcr (ossId) {
if (this.ocrDoneSet.has(ossId)) return
this.ocrDoneSet.add(ossId)
this.$set(this.ocrLoadingMap, ossId, true)
try {
const res = await ocrAppropriationInvoice(ossId)
@@ -396,13 +412,19 @@ export default {
amount: totalAmount,
sortNo: this.invoiceItems.length
})
} else {
// OCR返回无内容添加空条目供手动填写
this.invoiceItems.push({ ossId: ossId, itemName: '', reason: '', amount: 0, sortNo: this.invoiceItems.length })
}
this.recalcTotal()
} else {
// 接口无有效数据,添加空条目供手动填写
this.invoiceItems.push({ ossId: ossId, itemName: '', reason: '', amount: 0, sortNo: this.invoiceItems.length })
}
this.ocrDoneSet.add(ossId)
} catch (e) {
console.error('[OCR] 识别失败', e)
this.$message.warning('发票识别失败,请手动填写事由和金额')
this.invoiceItems.push({ ossId: ossId, itemName: '', reason: '', amount: 0, sortNo: this.invoiceItems.length })
} finally {
this.$set(this.ocrLoadingMap, ossId, false)
}
@@ -429,6 +451,21 @@ export default {
this.recalcTotal()
},
addManualItem () {
this.invoiceItems.push({ ossId: null, itemName: '', reason: '', amount: 0, sortNo: this.invoiceItems.length })
},
onRowFileSuccess (res, idx) {
if (res.code === 200 && res.data) {
const ossId = res.data.ossId
this.ocrDoneSet.add(ossId)
this.$set(this.invoiceItems[idx], 'ossId', ossId)
const ids = (this.form.accessoryApplyIds || '').split(',').filter(Boolean)
ids.push(ossId)
this.form.accessoryApplyIds = ids.join(',')
}
},
recalcTotal () {
const total = this.invoiceItems.reduce((sum, item) => sum + (parseFloat(item.amount) || 0), 0)
this.form.amount = parseFloat(total.toFixed(2))
@@ -676,12 +713,16 @@ export default {
.col-reason { flex: 1; }
.col-amount { width: 140px; flex-shrink: 0; }
.col-file { width: 64px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; }
.col-action { width: 32px; flex-shrink: 0; display: flex; justify-content: center; }
.invoice-table-header .col-reason { flex: 1; }
.invoice-table-header .col-amount { width: 140px; }
.invoice-table-header .col-file { width: 64px; }
.invoice-table-header .col-action { width: 32px; }
.row-upload { display: inline-flex; }
.invoice-table-footer {
display: flex;
align-items: center;

View File

@@ -77,12 +77,14 @@
<!-- 发票明细条目表 -->
<div class="block-title">
发票明细
<span class="block-title-hint">上传发票后自动识别删除条目将同步移除对应文件</span>
<span class="block-title-hint">上传发票后自动识别识别失败或无发票时可手动添加</span>
<el-button size="mini" type="primary" plain icon="el-icon-plus" @click="addManualItem" style="margin-left:12px;vertical-align:middle">手动添加条目</el-button>
</div>
<div class="invoice-table" v-if="invoiceItems.length">
<div class="invoice-table-header">
<span class="col-reason">事由说明</span>
<span class="col-amount">金额</span>
<span class="col-file">附件</span>
<span class="col-action"></span>
</div>
<div v-for="(item, idx) in invoiceItems" :key="idx" class="invoice-table-row">
@@ -100,6 +102,16 @@
class="col-amount"
@change="recalcTotal"
/>
<div class="col-file">
<el-tooltip v-if="item.ossId" content="已关联附件" placement="top">
<i class="el-icon-paperclip" style="color:#409eff"></i>
</el-tooltip>
<el-upload v-else :action="uploadFileUrl" :headers="uploadHeaders" :show-file-list="false"
:data="{ isPublic: 1 }" :on-success="(res) => onRowFileSuccess(res, idx)"
accept=".pdf,.jpg,.jpeg,.png" class="row-upload">
<el-button size="mini" type="text" icon="el-icon-paperclip"></el-button>
</el-upload>
</div>
<div class="col-action">
<el-button size="mini" type="danger" plain icon="el-icon-delete" circle @click="removeInvoiceItem(idx)" />
</div>
@@ -235,6 +247,7 @@ import { addReimburseReq, checkReimburseOcrHealth, listFlowNode, listFlowTemplat
import { getEmployeeByUserId } from '@/api/hrm/employee';
import { ccFlowTask } from '@/api/hrm/flow';
import FileUpload from '@/components/FileUpload';
import { getToken } from '@/utils/auth';
import UserMultiSelect from '@/components/UserSelect/multi.vue';
import UserSelect from '@/components/UserSelect/single.vue';
@@ -256,6 +269,8 @@ export default {
reimburseTypeOptions: ['差旅费', '招待费', '办公费', '交通费', '通讯费', '其他'],
// OCR服务状态null=检测中true=正常false=不可用
ocrAvailable: null,
uploadFileUrl: process.env.VUE_APP_BASE_API + '/system/oss/upload',
uploadHeaders: { Authorization: 'Bearer ' + getToken() },
// 发票明细条目
invoiceItems: [],
// OCR加载状态 { ossId: true/false }
@@ -342,6 +357,7 @@ export default {
async triggerOcr (ossId) {
if (this.ocrDoneSet.has(ossId)) return
this.ocrDoneSet.add(ossId)
this.$set(this.ocrLoadingMap, ossId, true)
try {
const res = await ocrReimburseInvoice(ossId)
@@ -370,13 +386,19 @@ export default {
amount: totalAmount,
sortNo: this.invoiceItems.length
})
} else {
// OCR返回无内容添加空条目供手动填写
this.invoiceItems.push({ ossId: ossId, itemName: '', reason: '', amount: 0, sortNo: this.invoiceItems.length })
}
this.recalcTotal()
} else {
// 接口无有效数据,添加空条目供手动填写
this.invoiceItems.push({ ossId: ossId, itemName: '', reason: '', amount: 0, sortNo: this.invoiceItems.length })
}
this.ocrDoneSet.add(ossId)
} catch (e) {
console.error('[OCR] 识别失败', e)
this.$message.warning('发票识别失败,请手动填写事由和金额')
this.invoiceItems.push({ ossId: ossId, itemName: '', reason: '', amount: 0, sortNo: this.invoiceItems.length })
} finally {
this.$set(this.ocrLoadingMap, ossId, false)
}
@@ -403,6 +425,21 @@ export default {
this.recalcTotal()
},
addManualItem () {
this.invoiceItems.push({ ossId: null, itemName: '', reason: '', amount: 0, sortNo: this.invoiceItems.length })
},
onRowFileSuccess (res, idx) {
if (res.code === 200 && res.data) {
const ossId = res.data.ossId
this.ocrDoneSet.add(ossId)
this.$set(this.invoiceItems[idx], 'ossId', ossId)
const ids = (this.form.accessoryApplyIds || '').split(',').filter(Boolean)
ids.push(ossId)
this.form.accessoryApplyIds = ids.join(',')
}
},
recalcTotal () {
const total = this.invoiceItems.reduce((sum, item) => sum + (parseFloat(item.amount) || 0), 0)
this.form.totalAmount = parseFloat(total.toFixed(2))
@@ -655,6 +692,14 @@ export default {
flex-shrink: 0;
}
.col-file {
width: 64px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
.col-action {
width: 32px;
flex-shrink: 0;
@@ -664,8 +709,11 @@ export default {
.invoice-table-header .col-reason { flex: 1; }
.invoice-table-header .col-amount { width: 140px; }
.invoice-table-header .col-file { width: 64px; }
.invoice-table-header .col-action { width: 32px; }
.row-upload { display: inline-flex; }
.invoice-table-footer {
display: flex;
align-items: center;