Merge: OCR失败降级手动填写 + 手动新增条目
This commit is contained in:
@@ -68,9 +68,11 @@ public class HrmAppropriationReqController extends BaseController {
|
|||||||
return R.ok(service.queryList(bo));
|
return R.ok(service.queryList(bo));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@GetMapping("/ocr-health")
|
||||||
* 通过ossId触发发票OCR识别,返回识别条目(不保存,供前端实时回显)
|
public R<Boolean> ocrHealth() {
|
||||||
*/
|
return R.ok(invoiceOcrService.isAlive());
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/ocr-by-oss")
|
@PostMapping("/ocr-by-oss")
|
||||||
public R<HrmInvoiceOcrResultVo> ocrByOss(@RequestParam @NotNull Long ossId) {
|
public R<HrmInvoiceOcrResultVo> ocrByOss(@RequestParam @NotNull Long ossId) {
|
||||||
return R.ok(invoiceOcrService.recognizeByOssId(ossId));
|
return R.ok(invoiceOcrService.recognizeByOssId(ossId));
|
||||||
|
|||||||
@@ -68,9 +68,11 @@ public class HrmReimburseReqController extends BaseController {
|
|||||||
return R.ok(service.queryList(bo));
|
return R.ok(service.queryList(bo));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@GetMapping("/ocr-health")
|
||||||
* 通过ossId触发发票OCR识别,返回识别条目(不保存,供前端实时回显)
|
public R<Boolean> ocrHealth() {
|
||||||
*/
|
return R.ok(invoiceOcrService.isAlive());
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/ocr-by-oss")
|
@PostMapping("/ocr-by-oss")
|
||||||
public R<HrmInvoiceOcrResultVo> ocrByOss(@RequestParam @NotNull Long ossId) {
|
public R<HrmInvoiceOcrResultVo> ocrByOss(@RequestParam @NotNull Long ossId) {
|
||||||
return R.ok(invoiceOcrService.recognizeByOssId(ossId));
|
return R.ok(invoiceOcrService.recognizeByOssId(ossId));
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ public interface IHrmInvoiceOcrService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 通过ossId识别发票
|
* 通过ossId识别发票
|
||||||
*
|
|
||||||
* @param ossId 附件ID
|
|
||||||
* @return 识别结果
|
|
||||||
*/
|
*/
|
||||||
HrmInvoiceOcrResultVo recognizeByOssId(Long ossId);
|
HrmInvoiceOcrResultVo recognizeByOssId(Long ossId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查OCR服务是否存活
|
||||||
|
*/
|
||||||
|
boolean isAlive();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -193,6 +193,20 @@ public class HrmInvoiceOcrServiceImpl implements IHrmInvoiceOcrService {
|
|||||||
return val.toString();
|
return val.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isAlive() {
|
||||||
|
String ocrUrl = ocrProperties.getUrl();
|
||||||
|
if (StringUtils.isBlank(ocrUrl)) return false;
|
||||||
|
try {
|
||||||
|
RestTemplate restTemplate = new RestTemplate();
|
||||||
|
ResponseEntity<String> resp = restTemplate.getForEntity(ocrUrl + "/health", String.class);
|
||||||
|
return resp.getStatusCode().is2xxSuccessful();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[OCR] 健康检查失败: {}", e.getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private String getStringOrFieldValue(JSONObject obj, String key) {
|
private String getStringOrFieldValue(JSONObject obj, String key) {
|
||||||
Object val = obj.get(key);
|
Object val = obj.get(key);
|
||||||
if (val == null) return null;
|
if (val == null) return null;
|
||||||
|
|||||||
@@ -58,6 +58,10 @@ export function getAppropriationStats (query) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function checkAppropriationOcrHealth () {
|
||||||
|
return request({ url: '/hrm/appropriation/ocr-health', method: 'get' })
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通过ossId触发发票OCR识别(返回识别条目,不保存)
|
* 通过ossId触发发票OCR识别(返回识别条目,不保存)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ export function allReimburseReq(query) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function checkReimburseOcrHealth() {
|
||||||
|
return request({ url: '/hrm/reimburse/ocr-health', method: 'get' })
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通过ossId触发发票OCR识别(返回识别条目,不保存)
|
* 通过ossId触发发票OCR识别(返回识别条目,不保存)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<el-upload multiple :drag="dragShow" :action="uploadFileUrl" :before-upload="handleBeforeUpload"
|
<el-upload multiple :drag="dragShow" :action="uploadFileUrl" :before-upload="handleBeforeUpload"
|
||||||
:file-list="fileList" :limit="limit" :on-error="handleUploadError" :on-exceed="handleExceed"
|
:file-list="fileList" :limit="limit" :on-error="handleUploadError" :on-exceed="handleExceed"
|
||||||
:on-success="handleUploadSuccess" :show-file-list="false" :data="{ isPublic: extraData }" :headers="headers"
|
:on-success="handleUploadSuccess" :show-file-list="false" :data="{ isPublic: extraData }" :headers="headers"
|
||||||
class="upload-file-uploader" ref="fileUpload">
|
:disabled="disabled" class="upload-file-uploader" ref="fileUpload">
|
||||||
<i class="el-icon-upload" v-if="dragShow"></i>
|
<i class="el-icon-upload" v-if="dragShow"></i>
|
||||||
<div class="el-upload__text" v-if="dragShow">
|
<div class="el-upload__text" v-if="dragShow">
|
||||||
将文件拖到此处,或<em>点击上传</em>
|
将文件拖到此处,或<em>点击上传</em>
|
||||||
@@ -71,6 +71,10 @@ export default {
|
|||||||
isShowTip: {
|
isShowTip: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
inject: ['$folder'],
|
inject: ['$folder'],
|
||||||
@@ -126,6 +130,7 @@ export default {
|
|||||||
this.$download.oss(file.ossId);
|
this.$download.oss(file.ossId);
|
||||||
},
|
},
|
||||||
handleBeforeUpload (file) {
|
handleBeforeUpload (file) {
|
||||||
|
if (this.disabled) return false;
|
||||||
const ext = file.name.split('.').pop().toLowerCase();
|
const ext = file.name.split('.').pop().toLowerCase();
|
||||||
if (this.fileType && !this.fileType.includes(ext)) {
|
if (this.fileType && !this.fileType.includes(ext)) {
|
||||||
this.$modal.msgError(`文件格式不正确,请上传${this.fileType.join('/')}格式文件!`);
|
this.$modal.msgError(`文件格式不正确,请上传${this.fileType.join('/')}格式文件!`);
|
||||||
|
|||||||
@@ -9,6 +9,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px" size="small" class="metal-form">
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px" size="small" class="metal-form">
|
||||||
|
<el-alert v-if="ocrAvailable === false" type="error" :closable="false" show-icon style="margin-bottom:14px">
|
||||||
|
<span slot="title">发票识别服务已停止,请联系信息化部门。在服务恢复前,您暂时无法上传发票附件。</span>
|
||||||
|
</el-alert>
|
||||||
|
|
||||||
<div class="form-summary">
|
<div class="form-summary">
|
||||||
<div class="summary-left">
|
<div class="summary-left">
|
||||||
<div class="summary-title">发起拨款申请</div>
|
<div class="summary-title">发起拨款申请</div>
|
||||||
@@ -57,8 +61,11 @@
|
|||||||
<el-form-item label="拨款单据附件" prop="accessoryApplyIds">
|
<el-form-item label="拨款单据附件" prop="accessoryApplyIds">
|
||||||
<file-upload v-model="form.accessoryApplyIds" :limit="50" :file-size="50"
|
<file-upload v-model="form.accessoryApplyIds" :limit="50" :file-size="50"
|
||||||
:file-type="['pdf', 'jpg', 'jpeg', 'png', 'doc', 'docx']" multiple
|
:file-type="['pdf', 'jpg', 'jpeg', 'png', 'doc', 'docx']" multiple
|
||||||
|
:disabled="ocrAvailable === false"
|
||||||
@delete="onFileDelete" />
|
@delete="onFileDelete" />
|
||||||
<div class="hint-text">上传发票、收据、付款截图等(支持 PDF/图片,上传后自动识别明细)</div>
|
<div class="hint-text">
|
||||||
|
{{ ocrAvailable === false ? '识别服务不可用,暂时无法上传' : '上传发票、收据、付款截图等(支持 PDF/图片,上传后自动识别明细)' }}
|
||||||
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<!-- OCR 识别中提示 -->
|
<!-- OCR 识别中提示 -->
|
||||||
@@ -70,12 +77,14 @@
|
|||||||
<!-- 发票明细条目表 -->
|
<!-- 发票明细条目表 -->
|
||||||
<div class="block-title">
|
<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>
|
||||||
<div class="invoice-table" v-if="invoiceItems.length">
|
<div class="invoice-table" v-if="invoiceItems.length">
|
||||||
<div class="invoice-table-header">
|
<div class="invoice-table-header">
|
||||||
<span class="col-reason">事由说明</span>
|
<span class="col-reason">事由说明</span>
|
||||||
<span class="col-amount">金额(元)</span>
|
<span class="col-amount">金额(元)</span>
|
||||||
|
<span class="col-file">附件</span>
|
||||||
<span class="col-action"></span>
|
<span class="col-action"></span>
|
||||||
</div>
|
</div>
|
||||||
<div v-for="(item, idx) in invoiceItems" :key="idx" class="invoice-table-row">
|
<div v-for="(item, idx) in invoiceItems" :key="idx" class="invoice-table-row">
|
||||||
@@ -93,6 +102,16 @@
|
|||||||
class="col-amount"
|
class="col-amount"
|
||||||
@change="recalcTotal"
|
@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">
|
<div class="col-action">
|
||||||
<el-button size="mini" type="danger" plain icon="el-icon-delete" circle @click="removeInvoiceItem(idx)" />
|
<el-button size="mini" type="danger" plain icon="el-icon-delete" circle @click="removeInvoiceItem(idx)" />
|
||||||
</div>
|
</div>
|
||||||
@@ -247,10 +266,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { addAppropriationReq, listFlowNode, listFlowTemplate, ocrAppropriationInvoice } from '@/api/hrm';
|
import { addAppropriationReq, checkAppropriationOcrHealth, listFlowNode, listFlowTemplate, ocrAppropriationInvoice } from '@/api/hrm';
|
||||||
import { getEmployeeByUserId } from '@/api/hrm/employee';
|
import { getEmployeeByUserId } from '@/api/hrm/employee';
|
||||||
import { ccFlowTask } from '@/api/hrm/flow';
|
import { ccFlowTask } from '@/api/hrm/flow';
|
||||||
import FileUpload from '@/components/FileUpload';
|
import FileUpload from '@/components/FileUpload';
|
||||||
|
import { getToken } from '@/utils/auth';
|
||||||
import UserMultiSelect from '@/components/UserSelect/multi.vue';
|
import UserMultiSelect from '@/components/UserSelect/multi.vue';
|
||||||
import UserSelect from '@/components/UserSelect/single.vue';
|
import UserSelect from '@/components/UserSelect/single.vue';
|
||||||
|
|
||||||
@@ -271,6 +291,11 @@ export default {
|
|||||||
assigneeUserName: '',
|
assigneeUserName: '',
|
||||||
appropriationTypeOptions: ['差旅费', '招待费', '办公费', '交通费', '通讯费', '其他'],
|
appropriationTypeOptions: ['差旅费', '招待费', '办公费', '交通费', '通讯费', '其他'],
|
||||||
// 发票明细条目
|
// 发票明细条目
|
||||||
|
// OCR服务状态:null=检测中,true=正常,false=不可用
|
||||||
|
ocrAvailable: null,
|
||||||
|
uploadFileUrl: process.env.VUE_APP_BASE_API + '/system/oss/upload',
|
||||||
|
uploadHeaders: { Authorization: 'Bearer ' + getToken() },
|
||||||
|
// 发票明细条目
|
||||||
invoiceItems: [],
|
invoiceItems: [],
|
||||||
// OCR加载状态 { ossId: true/false }
|
// OCR加载状态 { ossId: true/false }
|
||||||
ocrLoadingMap: {},
|
ocrLoadingMap: {},
|
||||||
@@ -309,6 +334,7 @@ export default {
|
|||||||
created () {
|
created () {
|
||||||
this.loadCurrentEmployee()
|
this.loadCurrentEmployee()
|
||||||
this.loadTemplates()
|
this.loadTemplates()
|
||||||
|
this.checkOcrHealth()
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
currentApplicantText () {
|
currentApplicantText () {
|
||||||
@@ -346,8 +372,18 @@ export default {
|
|||||||
onCcUsersSelected (users) { this.ccForm.selectedUsers = users || [] },
|
onCcUsersSelected (users) { this.ccForm.selectedUsers = users || [] },
|
||||||
removeCcUser (user) { this.ccForm.selectedUsers = this.ccForm.selectedUsers.filter(u => u.userId !== user.userId) },
|
removeCcUser (user) { this.ccForm.selectedUsers = this.ccForm.selectedUsers.filter(u => u.userId !== user.userId) },
|
||||||
|
|
||||||
|
async checkOcrHealth () {
|
||||||
|
try {
|
||||||
|
const res = await checkAppropriationOcrHealth()
|
||||||
|
this.ocrAvailable = res.code === 200 && res.data === true
|
||||||
|
} catch (e) {
|
||||||
|
this.ocrAvailable = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async triggerOcr (ossId) {
|
async triggerOcr (ossId) {
|
||||||
if (this.ocrDoneSet.has(ossId)) return
|
if (this.ocrDoneSet.has(ossId)) return
|
||||||
|
this.ocrDoneSet.add(ossId)
|
||||||
this.$set(this.ocrLoadingMap, ossId, true)
|
this.$set(this.ocrLoadingMap, ossId, true)
|
||||||
try {
|
try {
|
||||||
const res = await ocrAppropriationInvoice(ossId)
|
const res = await ocrAppropriationInvoice(ossId)
|
||||||
@@ -376,13 +412,19 @@ export default {
|
|||||||
amount: totalAmount,
|
amount: totalAmount,
|
||||||
sortNo: this.invoiceItems.length
|
sortNo: this.invoiceItems.length
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
// OCR返回无内容,添加空条目供手动填写
|
||||||
|
this.invoiceItems.push({ ossId: ossId, itemName: '', reason: '', amount: 0, sortNo: this.invoiceItems.length })
|
||||||
}
|
}
|
||||||
this.recalcTotal()
|
this.recalcTotal()
|
||||||
|
} else {
|
||||||
|
// 接口无有效数据,添加空条目供手动填写
|
||||||
|
this.invoiceItems.push({ ossId: ossId, itemName: '', reason: '', amount: 0, sortNo: this.invoiceItems.length })
|
||||||
}
|
}
|
||||||
this.ocrDoneSet.add(ossId)
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[OCR] 识别失败', e)
|
console.error('[OCR] 识别失败', e)
|
||||||
this.$message.warning('发票识别失败,请手动填写事由和金额')
|
this.$message.warning('发票识别失败,请手动填写事由和金额')
|
||||||
|
this.invoiceItems.push({ ossId: ossId, itemName: '', reason: '', amount: 0, sortNo: this.invoiceItems.length })
|
||||||
} finally {
|
} finally {
|
||||||
this.$set(this.ocrLoadingMap, ossId, false)
|
this.$set(this.ocrLoadingMap, ossId, false)
|
||||||
}
|
}
|
||||||
@@ -409,6 +451,21 @@ export default {
|
|||||||
this.recalcTotal()
|
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 () {
|
recalcTotal () {
|
||||||
const total = this.invoiceItems.reduce((sum, item) => sum + (parseFloat(item.amount) || 0), 0)
|
const total = this.invoiceItems.reduce((sum, item) => sum + (parseFloat(item.amount) || 0), 0)
|
||||||
this.form.amount = parseFloat(total.toFixed(2))
|
this.form.amount = parseFloat(total.toFixed(2))
|
||||||
@@ -656,12 +713,16 @@ export default {
|
|||||||
|
|
||||||
.col-reason { flex: 1; }
|
.col-reason { flex: 1; }
|
||||||
.col-amount { width: 140px; flex-shrink: 0; }
|
.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; }
|
.col-action { width: 32px; flex-shrink: 0; display: flex; justify-content: center; }
|
||||||
|
|
||||||
.invoice-table-header .col-reason { flex: 1; }
|
.invoice-table-header .col-reason { flex: 1; }
|
||||||
.invoice-table-header .col-amount { width: 140px; }
|
.invoice-table-header .col-amount { width: 140px; }
|
||||||
|
.invoice-table-header .col-file { width: 64px; }
|
||||||
.invoice-table-header .col-action { width: 32px; }
|
.invoice-table-header .col-action { width: 32px; }
|
||||||
|
|
||||||
|
.row-upload { display: inline-flex; }
|
||||||
|
|
||||||
.invoice-table-footer {
|
.invoice-table-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -9,6 +9,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px" size="small" class="metal-form">
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px" size="small" class="metal-form">
|
||||||
|
<el-alert v-if="ocrAvailable === false" type="error" :closable="false" show-icon style="margin-bottom:14px">
|
||||||
|
<span slot="title">发票识别服务已停止,请联系信息化部门。在服务恢复前,您暂时无法上传发票附件。</span>
|
||||||
|
</el-alert>
|
||||||
|
|
||||||
<div class="form-summary">
|
<div class="form-summary">
|
||||||
<div class="summary-left">
|
<div class="summary-left">
|
||||||
<div class="summary-title">发起日常报销</div>
|
<div class="summary-title">发起日常报销</div>
|
||||||
@@ -57,8 +61,11 @@
|
|||||||
<el-form-item label="报销单据附件" prop="accessoryApplyIds">
|
<el-form-item label="报销单据附件" prop="accessoryApplyIds">
|
||||||
<file-upload v-model="form.accessoryApplyIds" :limit="200" :file-size="50"
|
<file-upload v-model="form.accessoryApplyIds" :limit="200" :file-size="50"
|
||||||
:file-type="['pdf', 'jpg', 'jpeg', 'png', 'doc', 'docx']" multiple
|
:file-type="['pdf', 'jpg', 'jpeg', 'png', 'doc', 'docx']" multiple
|
||||||
|
:disabled="ocrAvailable === false"
|
||||||
@delete="onFileDelete" />
|
@delete="onFileDelete" />
|
||||||
<div class="hint-text">上传发票、收据、付款截图等(支持 PDF/图片,上传后自动识别明细)</div>
|
<div class="hint-text">
|
||||||
|
{{ ocrAvailable === false ? '识别服务不可用,暂时无法上传' : '上传发票、收据、付款截图等(支持 PDF/图片,上传后自动识别明细)' }}
|
||||||
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<!-- OCR 识别中提示 -->
|
<!-- OCR 识别中提示 -->
|
||||||
@@ -70,12 +77,14 @@
|
|||||||
<!-- 发票明细条目表 -->
|
<!-- 发票明细条目表 -->
|
||||||
<div class="block-title">
|
<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>
|
||||||
<div class="invoice-table" v-if="invoiceItems.length">
|
<div class="invoice-table" v-if="invoiceItems.length">
|
||||||
<div class="invoice-table-header">
|
<div class="invoice-table-header">
|
||||||
<span class="col-reason">事由说明</span>
|
<span class="col-reason">事由说明</span>
|
||||||
<span class="col-amount">金额(元)</span>
|
<span class="col-amount">金额(元)</span>
|
||||||
|
<span class="col-file">附件</span>
|
||||||
<span class="col-action"></span>
|
<span class="col-action"></span>
|
||||||
</div>
|
</div>
|
||||||
<div v-for="(item, idx) in invoiceItems" :key="idx" class="invoice-table-row">
|
<div v-for="(item, idx) in invoiceItems" :key="idx" class="invoice-table-row">
|
||||||
@@ -93,6 +102,16 @@
|
|||||||
class="col-amount"
|
class="col-amount"
|
||||||
@change="recalcTotal"
|
@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">
|
<div class="col-action">
|
||||||
<el-button size="mini" type="danger" plain icon="el-icon-delete" circle @click="removeInvoiceItem(idx)" />
|
<el-button size="mini" type="danger" plain icon="el-icon-delete" circle @click="removeInvoiceItem(idx)" />
|
||||||
</div>
|
</div>
|
||||||
@@ -224,10 +243,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { addReimburseReq, listFlowNode, listFlowTemplate, ocrReimburseInvoice } from '@/api/hrm';
|
import { addReimburseReq, checkReimburseOcrHealth, listFlowNode, listFlowTemplate, ocrReimburseInvoice } from '@/api/hrm';
|
||||||
import { getEmployeeByUserId } from '@/api/hrm/employee';
|
import { getEmployeeByUserId } from '@/api/hrm/employee';
|
||||||
import { ccFlowTask } from '@/api/hrm/flow';
|
import { ccFlowTask } from '@/api/hrm/flow';
|
||||||
import FileUpload from '@/components/FileUpload';
|
import FileUpload from '@/components/FileUpload';
|
||||||
|
import { getToken } from '@/utils/auth';
|
||||||
import UserMultiSelect from '@/components/UserSelect/multi.vue';
|
import UserMultiSelect from '@/components/UserSelect/multi.vue';
|
||||||
import UserSelect from '@/components/UserSelect/single.vue';
|
import UserSelect from '@/components/UserSelect/single.vue';
|
||||||
|
|
||||||
@@ -247,6 +267,10 @@ export default {
|
|||||||
assigneeUserId: null,
|
assigneeUserId: null,
|
||||||
assigneeUserName: '',
|
assigneeUserName: '',
|
||||||
reimburseTypeOptions: ['差旅费', '招待费', '办公费', '交通费', '通讯费', '其他'],
|
reimburseTypeOptions: ['差旅费', '招待费', '办公费', '交通费', '通讯费', '其他'],
|
||||||
|
// OCR服务状态:null=检测中,true=正常,false=不可用
|
||||||
|
ocrAvailable: null,
|
||||||
|
uploadFileUrl: process.env.VUE_APP_BASE_API + '/system/oss/upload',
|
||||||
|
uploadHeaders: { Authorization: 'Bearer ' + getToken() },
|
||||||
// 发票明细条目
|
// 发票明细条目
|
||||||
invoiceItems: [],
|
invoiceItems: [],
|
||||||
// OCR加载状态 { ossId: true/false }
|
// OCR加载状态 { ossId: true/false }
|
||||||
@@ -284,6 +308,7 @@ export default {
|
|||||||
created () {
|
created () {
|
||||||
this.loadCurrentEmployee()
|
this.loadCurrentEmployee()
|
||||||
this.loadTemplates()
|
this.loadTemplates()
|
||||||
|
this.checkOcrHealth()
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
currentApplicantText () {
|
currentApplicantText () {
|
||||||
@@ -321,8 +346,18 @@ export default {
|
|||||||
onCcUsersSelected (users) { this.ccForm.selectedUsers = users || [] },
|
onCcUsersSelected (users) { this.ccForm.selectedUsers = users || [] },
|
||||||
removeCcUser (user) { this.ccForm.selectedUsers = this.ccForm.selectedUsers.filter(u => u.userId !== user.userId) },
|
removeCcUser (user) { this.ccForm.selectedUsers = this.ccForm.selectedUsers.filter(u => u.userId !== user.userId) },
|
||||||
|
|
||||||
|
async checkOcrHealth () {
|
||||||
|
try {
|
||||||
|
const res = await checkReimburseOcrHealth()
|
||||||
|
this.ocrAvailable = res.code === 200 && res.data === true
|
||||||
|
} catch (e) {
|
||||||
|
this.ocrAvailable = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
async triggerOcr (ossId) {
|
async triggerOcr (ossId) {
|
||||||
if (this.ocrDoneSet.has(ossId)) return
|
if (this.ocrDoneSet.has(ossId)) return
|
||||||
|
this.ocrDoneSet.add(ossId)
|
||||||
this.$set(this.ocrLoadingMap, ossId, true)
|
this.$set(this.ocrLoadingMap, ossId, true)
|
||||||
try {
|
try {
|
||||||
const res = await ocrReimburseInvoice(ossId)
|
const res = await ocrReimburseInvoice(ossId)
|
||||||
@@ -351,13 +386,19 @@ export default {
|
|||||||
amount: totalAmount,
|
amount: totalAmount,
|
||||||
sortNo: this.invoiceItems.length
|
sortNo: this.invoiceItems.length
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
// OCR返回无内容,添加空条目供手动填写
|
||||||
|
this.invoiceItems.push({ ossId: ossId, itemName: '', reason: '', amount: 0, sortNo: this.invoiceItems.length })
|
||||||
}
|
}
|
||||||
this.recalcTotal()
|
this.recalcTotal()
|
||||||
|
} else {
|
||||||
|
// 接口无有效数据,添加空条目供手动填写
|
||||||
|
this.invoiceItems.push({ ossId: ossId, itemName: '', reason: '', amount: 0, sortNo: this.invoiceItems.length })
|
||||||
}
|
}
|
||||||
this.ocrDoneSet.add(ossId)
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[OCR] 识别失败', e)
|
console.error('[OCR] 识别失败', e)
|
||||||
this.$message.warning('发票识别失败,请手动填写事由和金额')
|
this.$message.warning('发票识别失败,请手动填写事由和金额')
|
||||||
|
this.invoiceItems.push({ ossId: ossId, itemName: '', reason: '', amount: 0, sortNo: this.invoiceItems.length })
|
||||||
} finally {
|
} finally {
|
||||||
this.$set(this.ocrLoadingMap, ossId, false)
|
this.$set(this.ocrLoadingMap, ossId, false)
|
||||||
}
|
}
|
||||||
@@ -384,6 +425,21 @@ export default {
|
|||||||
this.recalcTotal()
|
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 () {
|
recalcTotal () {
|
||||||
const total = this.invoiceItems.reduce((sum, item) => sum + (parseFloat(item.amount) || 0), 0)
|
const total = this.invoiceItems.reduce((sum, item) => sum + (parseFloat(item.amount) || 0), 0)
|
||||||
this.form.totalAmount = parseFloat(total.toFixed(2))
|
this.form.totalAmount = parseFloat(total.toFixed(2))
|
||||||
@@ -636,6 +692,14 @@ export default {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.col-file {
|
||||||
|
width: 64px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
.col-action {
|
.col-action {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
@@ -645,8 +709,11 @@ export default {
|
|||||||
|
|
||||||
.invoice-table-header .col-reason { flex: 1; }
|
.invoice-table-header .col-reason { flex: 1; }
|
||||||
.invoice-table-header .col-amount { width: 140px; }
|
.invoice-table-header .col-amount { width: 140px; }
|
||||||
|
.invoice-table-header .col-file { width: 64px; }
|
||||||
.invoice-table-header .col-action { width: 32px; }
|
.invoice-table-header .col-action { width: 32px; }
|
||||||
|
|
||||||
|
.row-upload { display: inline-flex; }
|
||||||
|
|
||||||
.invoice-table-footer {
|
.invoice-table-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user