1、新增结算模块

2、调整业务模块
3、调整客商模块
This commit is contained in:
2026-08-20 06:33:01 +08:00
parent 4199255186
commit fd5b1484e6
59 changed files with 11745 additions and 1104 deletions
+178 -35
View File
@@ -101,19 +101,20 @@
</div>
<div class="insurance-record-page__upload-text">
<strong>上传图片,自动识别填写</strong>
<span>支持JPGPNGPDF上传</span>
<span>支持JPGPNGBMP上传</span>
</div>
</div>
<el-upload
v-if="hasPermission('insurance_record_ocr')"
class="insurance-record-page__upload-action"
:action="recognizeUrl"
:data="recognizeData"
action="/api/blade-resource/oss/endpoint/put-file"
name="file"
:headers="uploadHeaders"
:show-file-list="false"
:on-success="handleRecognizeSuccess"
:on-error="handleRecognizeError"
accept=".jpg,.jpeg,.png,.pdf"
:on-success="handlePolicyUploadSuccess"
:on-error="handlePolicyUploadError"
:before-upload="validateOcrUpload"
accept=".jpg,.jpeg,.png,.bmp"
>
<el-button class="insurance-record-page__upload-button" type="primary" plain>
上传文件
@@ -131,19 +132,20 @@
</div>
<div class="insurance-record-page__upload-text">
<strong>上传图片,自动识别填写</strong>
<span>支持JPGPNGPDF上传</span>
<span>支持JPGPNGBMP上传</span>
</div>
</div>
<el-upload
v-if="hasPermission('insurance_record_ocr')"
class="insurance-record-page__upload-action"
:action="recognizeUrl"
:data="recognizeData"
action="/api/blade-resource/oss/endpoint/put-file"
name="file"
:headers="uploadHeaders"
:show-file-list="false"
:on-success="handleRecognizeSuccess"
:on-error="handleRecognizeError"
accept=".jpg,.jpeg,.png,.pdf"
:on-success="handlePolicyUploadSuccess"
:on-error="handlePolicyUploadError"
:before-upload="validateOcrUpload"
accept=".jpg,.jpeg,.png,.bmp"
>
<el-button class="insurance-record-page__upload-button" type="primary" plain>
上传文件
@@ -171,9 +173,17 @@
</template>
<script>
import { add, getDetail, getList, remove, update } from '@/api/vehicle/insurance-record';
import {
add,
getDetail,
getList,
recognizeGeneralOcr,
remove,
update,
} from '@/api/vehicle/insurance-record';
import { getList as getVehicleList } from '@/api/transportCapacity/transport-vehicle';
import { getList as getShipList } from '@/api/transportCapacity/transport-ship';
import { getList as getOcrTemplateList } from '@/api/base/insurance-ocr-template';
import { getDeptTree } from '@/api/system/dept';
import { exportBlob } from '@/api/common';
import { downloadXls } from '@/utils/util';
@@ -182,6 +192,7 @@ import { getToken } from '@/utils/auth';
import { getUploadHeaders } from '@/utils/upload';
import { normalizeSearchRangeParams } from '@/utils/search-range';
import { mapGetters } from 'vuex';
import { ElLoading } from 'element-plus';
import { excelOption, option } from '@/option/vehicle/insurance-record';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
@@ -210,10 +221,12 @@ export default {
},
selectionList: [],
data: [],
ocrTemplateOptions: [],
};
},
created() {
this.initDeptTree();
this.initOcrTemplateOptions();
},
computed: {
...mapGetters(['permission', 'userInfo']),
@@ -239,15 +252,6 @@ export default {
});
return ids.join(',');
},
recognizeUrl() {
return '/blade-transport/insurance-record/recognize';
},
recognizeData() {
return {
vehicleType: this.form.vehicleType || '车辆',
ocrTemplate: this.form.ocrTemplate,
};
},
uploadHeaders() {
return getUploadHeaders();
},
@@ -278,6 +282,22 @@ export default {
column.dicData = res.data.data;
});
},
initOcrTemplateOptions() {
getOcrTemplateList(1, 100, {})
.then(res => {
const records = res.data.data?.records || [];
this.ocrTemplateOptions = records.map(item => ({
label: item.name,
value: item.name,
mappingConfig: item.mappingConfig || '[]',
}));
const templateColumn = this.findColumn(this.option.column, 'ocrTemplate');
if (templateColumn) templateColumn.dicData = this.ocrTemplateOptions;
})
.catch(() => {
this.ocrTemplateOptions = [];
});
},
fetchVehicleOptions(queryString, callback) {
const vehicleType = this.form.vehicleType || '车辆';
const request = vehicleType === '船舶' ? getShipList : getVehicleList;
@@ -402,7 +422,7 @@ export default {
this.boxType = type;
if (type === 'add') {
this.form.vehicleType = '车辆';
this.form.ocrTemplate = '紫金-机动车交强险';
this.form.ocrTemplate = this.ocrTemplateOptions[0]?.value || '';
}
if (['edit', 'view'].includes(type)) {
this.isDetailLoading = true;
@@ -499,20 +519,143 @@ export default {
downloadXls(res.data, '保险记录模板.xlsx');
});
},
handleRecognizeSuccess(res) {
if (res && res.success && res.data) {
this.form = {
...this.form,
...res.data,
vehicleType: res.data.vehicleType || this.form.vehicleType || '车辆',
};
this.$message.success('识别完成');
} else {
this.$message.warning((res && res.msg) || '识别失败,请手动填写');
handlePolicyUploadSuccess(res) {
if (res?.code !== 200 || !res.data) {
this.$message.error(res?.msg || '上传失败');
return;
}
const imageUrl = res.data.link || res.data.url || res.data.domain || '';
if (!imageUrl) {
this.$message.warning('文件上传成功,未获取到文件地址,无法自动识别');
return;
}
this.form.policyFile = imageUrl;
this.recognizePolicyFile(imageUrl);
},
handlePolicyUploadError() {
this.$message.error('上传失败');
},
recognizePolicyFile(imageUrl) {
const loading = ElLoading.service({
lock: true,
text: '保单识别中',
background: 'rgba(255, 255, 255, 0.7)',
});
recognizeGeneralOcr(imageUrl)
.then(res => {
const result = res.data.data?.result || {};
const filledCount = this.applyTemplateRecognition(result);
if (filledCount > 0) {
this.$message.success(`保单识别完成,已填充${filledCount}`);
} else {
this.$message.warning('保单识别完成,未匹配到模板字段,请手动填写保险信息');
}
})
.catch(() => {
this.$message.warning('文件上传成功,自动识别失败,请手动填写保险信息');
})
.finally(() => {
loading.close();
});
},
applyTemplateRecognition(data = {}) {
const template = this.ocrTemplateOptions.find(item => item.value === this.form.ocrTemplate);
const mappings = this.parseTemplateMappings(template?.mappingConfig);
const words = this.getGeneralOcrWords(data);
const fieldPropMap = {
保险类型: 'insuranceType',
保单号: 'policyNo',
开始日期: 'startDate',
结束日期: 'endDate',
保额: 'insuredAmount',
保费: 'premium',
发票号: 'invoiceNo',
开票日期: 'invoiceDate',
备注: 'remark',
};
let filledCount = 0;
mappings.forEach(item => {
const prop = fieldPropMap[item.key];
const value = this.getMappedOcrValue(words, item.value);
if (!prop || !value) return;
this.form[prop] = this.normalizeRecognizedValue(prop, value);
filledCount += 1;
});
return filledCount;
},
parseTemplateMappings(mappingConfig) {
try {
const mappings = JSON.parse(mappingConfig || '[]');
return Array.isArray(mappings)
? mappings.filter(
item => String(item?.key || '').trim() && String(item?.value || '').trim()
)
: [];
} catch (error) {
return [];
}
},
handleRecognizeError() {
this.$message.warning('识别失败,请手动填写');
getGeneralOcrWords(data = {}) {
const wordsResult = data.words_result || data.wordsResult || [];
if (Array.isArray(wordsResult)) {
return wordsResult.map(item => item.words || item.value || '').filter(Boolean);
}
return Object.entries(wordsResult)
.flatMap(([key, value]) => [
key,
typeof value === 'object' ? value.words || value.value || '' : value,
])
.filter(Boolean);
},
getMappedOcrValue(words = [], mappingLabel = '') {
const label = String(mappingLabel || '').trim();
if (!label) return '';
const escapedLabel = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const normalizedWords = words.map(word => String(word || '').trim()).filter(Boolean);
const exactIndex = normalizedWords.findIndex(word =>
new RegExp(`^${escapedLabel}\\s*[:]?$`).test(word)
);
if (exactIndex >= 0) return normalizedWords[exactIndex + 1] || '';
const valueLine = normalizedWords.find(word =>
new RegExp(`${escapedLabel}\\s*[:]?\\s*(.+)$`).test(word)
);
if (!valueLine) return '';
return valueLine.replace(new RegExp(`^.*?${escapedLabel}\\s*[:]?\\s*`), '').trim();
},
normalizeRecognizedValue(prop, value) {
const text = String(value || '').trim();
if (['startDate', 'endDate', 'invoiceDate'].includes(prop)) {
const match = text.match(/(\d{4})[-/.年](\d{1,2})[-/.月](\d{1,2})日?/);
return match
? `${match[1]}-${match[2].padStart(2, '0')}-${match[3].padStart(2, '0')}`
: text;
}
if (['insuredAmount', 'premium'].includes(prop)) {
return text.replace(/,/g, '').replace(/[^\d.]/g, '');
}
if (prop === 'insuranceType') {
const typeRules = [
{ value: '交强险', pattern: /(交强险|交通事故责任强制保险)/ },
{ value: '商业险', pattern: /(商业险|商业保险)/ },
{ value: '承运人责任险', pattern: /承运人.*责任/ },
{ value: '货运险', pattern: /(货运险|货物运输保险)/ },
{ value: '船舶险', pattern: /(船舶险|船舶保险)/ },
];
return typeRules.find(item => item.pattern.test(text))?.value || text;
}
if (prop === 'remark') return text.slice(0, 200);
return text;
},
validateOcrUpload(file) {
if (!this.form.ocrTemplate) {
this.$message.warning('请先选择OCR识别模板');
return false;
}
const validType = ['image/jpeg', 'image/png', 'image/jpg', 'image/bmp'].includes(file.type);
const validSize = file.size / 1024 / 1024 < 5;
if (!validType) this.$message.error('仅支持 JPG、PNG、BMP 图片');
if (!validSize) this.$message.error('图片大小不能超过 5MB');
return validType && validSize;
},
},
};