Files
tms-erp-web/src/views/vehicle/insurance-record.vue
T
b2894lxlx 50c1afe9c8 ✨ 列表导出补齐创建人创建时间,并修复组织类型提交
多模块列表与导出展示创建人、创建时间;组织类型强制提交数字编码,避免中文 dictKey 反序列化失败。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-30 14:22:39 +08:00

1002 lines
32 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<basic-container class="insurance-record-page">
<avue-crud
:option="option"
:table-loading="loading"
:data="data"
v-model:page="page"
:permission="permissionList"
:before-open="beforeOpen"
v-model="form"
ref="crud"
@row-update="rowUpdate"
@row-save="rowSave"
@row-del="rowDel"
@search-change="searchChange"
@search-reset="searchReset"
@selection-change="selectionChange"
@current-change="currentChange"
@size-change="sizeChange"
@refresh-change="refreshChange"
@on-load="onLoad"
>
<template #menu-left>
<el-button
type="primary"
icon="el-icon-upload"
plain
v-if="hasPermission('insurance_record_import')"
@click="handleImport"
>批量导入
</el-button>
<el-button
type="primary"
icon="el-icon-download"
plain
v-if="hasPermission('insurance_record_template')"
@click="handleTemplate"
>下载模板
</el-button>
<el-button
type="primary"
icon="el-icon-download"
plain
v-if="hasPermission('insurance_record_export')"
@click="handleExport"
>批量导出
</el-button>
<el-button
type="danger"
icon="el-icon-delete"
plain
v-if="hasPermission('insurance_record_delete')"
@click="handleDelete"
>批量删除
</el-button>
</template>
<template #vehicleNo="{ row, index }">
<el-button type="primary" link @click="$refs.crud.rowView(row, index)">
{{ row.vehicleNo }}
</el-button>
</template>
<template #vehicleNoForm>
<el-select
v-model="form.vehicleNo"
:disabled="boxType === 'view'"
:loading="vehicleOptionsLoading"
:placeholder="vehicleNoPlaceholder"
clearable
filterable
remote
:remote-method="loadVehicleOptions"
@visible-change="handleVehicleSelectVisible"
>
<el-option
v-for="item in vehicleOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
<template #vehicleNo-form>
<el-select
v-model="form.vehicleNo"
:disabled="boxType === 'view'"
:loading="vehicleOptionsLoading"
:placeholder="vehicleNoPlaceholder"
clearable
filterable
remote
:remote-method="loadVehicleOptions"
@visible-change="handleVehicleSelectVisible"
>
<el-option
v-for="item in vehicleOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
<template #insuredAmountForm>
<el-input
v-model="form.insuredAmount"
:disabled="boxType === 'view'"
inputmode="decimal"
placeholder="请输入"
@input="value => normalizeDecimalInput('insuredAmount', value)"
>
<template #append>元</template>
</el-input>
</template>
<template #premiumForm>
<el-input
v-model="form.premium"
:disabled="boxType === 'view'"
inputmode="decimal"
placeholder="请输入"
@input="value => normalizeDecimalInput('premium', value)"
>
<template #append>元</template>
</el-input>
</template>
<template #policyFileForm>
<div class="insurance-record-page__upload">
<div class="insurance-record-page__upload-info">
<div class="insurance-record-page__upload-icon" aria-hidden="true">
<span class="insurance-record-page__upload-card">
<span></span>
</span>
</div>
<div class="insurance-record-page__upload-text">
<strong>上传图片,自动识别填写</strong>
<span>支持JPG、PNG、BMP上传</span>
</div>
</div>
<el-upload
v-if="hasPermission('insurance_record_ocr')"
class="insurance-record-page__upload-action"
action="/api/blade-resource/oss/endpoint/put-file"
name="file"
:headers="uploadHeaders"
:show-file-list="false"
: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>
上传文件
</el-button>
</el-upload>
</div>
</template>
<template #policyFile-form>
<div class="insurance-record-page__upload">
<div class="insurance-record-page__upload-info">
<div class="insurance-record-page__upload-icon" aria-hidden="true">
<span class="insurance-record-page__upload-card">
<span></span>
</span>
</div>
<div class="insurance-record-page__upload-text">
<strong>上传图片,自动识别填写</strong>
<span>支持JPG、PNG、BMP上传</span>
</div>
</div>
<el-upload
v-if="hasPermission('insurance_record_ocr')"
class="insurance-record-page__upload-action"
action="/api/blade-resource/oss/endpoint/put-file"
name="file"
:headers="uploadHeaders"
:show-file-list="false"
: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>
上传文件
</el-button>
</el-upload>
</div>
</template>
</avue-crud>
<empty-pagination
:page="page"
@size-change="sizeChange"
@current-change="currentChange"
@load="onLoad(page, query)"
/>
<el-dialog title="保险记录数据导入" append-to-body v-model="excelBox" width="555px">
<avue-form :option="excelOption" v-model="excelForm">
<template #excelTemplate>
<el-button type="primary" @click="handleTemplate">
点击下载<i class="el-icon-download el-icon--right"></i>
</el-button>
</template>
</avue-form>
</el-dialog>
</basic-container>
</template>
<script>
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';
import { handleImportExcel } from '@/utils/import-excel';
import { getToken, isAdministrator } 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, exportColumns, option } from '@/option/vehicle/insurance-record';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
const createTimeRangeMap = {
createTimeRange: ['createTimeStart', 'createTimeEnd'],
};
export default {
data() {
return {
form: {},
query: {},
loading: true,
excelBox: false,
excelForm: {},
isDetailLoading: false,
boxType: '',
vehicleOptions: [],
vehicleOptionsLoading: false,
vehicleOptionsRequestId: 0,
option,
excelOption,
page: {
pageSize: 10,
pageSizes: [10, 20, 50, 100],
currentPage: 1,
total: 0,
},
selectionList: [],
data: [],
ocrTemplateOptions: [],
};
},
created() {
this.initDeptTree();
this.initOcrTemplateOptions('车辆');
},
computed: {
...mapGetters(['permission', 'userInfo']),
isAdmin() {
const authority = this.userInfo.authority || '';
return isAdministrator(this.userInfo);
},
permissionList() {
return {
addBtn: this.hasPermission('insurance_record_add'),
viewBtn: this.hasPermission('insurance_record_view'),
delBtn: this.hasPermission('insurance_record_delete'),
editBtn: this.hasPermission('insurance_record_edit'),
};
},
vehicleNoPlaceholder() {
return this.form.vehicleType === '船舶' ? '请选择船号' : '请选择车牌号';
},
ids() {
const ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
uploadHeaders() {
return getUploadHeaders();
},
},
watch: {
'form.vehicleType': {
handler(value, oldValue) {
this.updateVehicleTypeDisplays(value || '车辆', oldValue);
},
immediate: true,
},
},
methods: {
hasPermission(code) {
return this.isAdmin || this.validData(this.permission[code], false);
},
updateVehicleTypeDisplays(vehicleType, oldValue) {
const vehicleNoColumn = this.findColumn(this.option.column, 'vehicleNo');
vehicleNoColumn.placeholder =
vehicleType === '船舶' ? '请选择船号' : '请选择车牌号';
if (!this.isDetailLoading && oldValue && vehicleType !== oldValue) {
this.form.vehicleNo = '';
this.vehicleOptions = [];
this.loadVehicleOptions('');
this.initOcrTemplateOptions(vehicleType, true);
}
},
initDeptTree() {
getDeptTree(this.userInfo.tenantId).then(res => {
const column = this.findColumn(this.option.column, 'createDept');
column.dicData = res.data.data;
});
},
initOcrTemplateOptions(vehicleType = '车辆', resetSelected = false) {
const type = vehicleType || '车辆';
return getOcrTemplateList(1, 100, { vehicleType: type })
.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;
if (resetSelected || !this.ocrTemplateOptions.some(item => item.value === this.form.ocrTemplate)) {
this.form.ocrTemplate = this.ocrTemplateOptions[0]?.value || '';
}
})
.catch(() => {
this.ocrTemplateOptions = [];
const templateColumn = this.findColumn(this.option.column, 'ocrTemplate');
if (templateColumn) templateColumn.dicData = [];
if (resetSelected) this.form.ocrTemplate = '';
});
},
handleVehicleSelectVisible(visible) {
if (visible && !this.vehicleOptions.length) this.loadVehicleOptions('');
},
loadVehicleOptions(queryString = '') {
const vehicleType = this.form.vehicleType || '车辆';
const isShip = vehicleType === '船舶';
const request = isShip ? getShipList : getVehicleList;
const params = isShip ? { shipIdentifierNo: queryString } : { plateNo: queryString };
const requestId = ++this.vehicleOptionsRequestId;
this.vehicleOptionsLoading = true;
request(1, 20, params)
.then(res => {
if (requestId !== this.vehicleOptionsRequestId) return;
const values = (res.data.data.records || [])
.map(item => (isShip ? item.shipIdentifierNo || item.shipName : item.plateNo))
.filter(Boolean);
this.vehicleOptions = [...new Set(values)].map(value => ({ label: value, value }));
if (this.form.vehicleNo && !this.vehicleOptions.some(item => item.value === this.form.vehicleNo)) {
this.vehicleOptions.unshift({ label: this.form.vehicleNo, value: this.form.vehicleNo });
}
})
.catch(() => {
if (requestId === this.vehicleOptionsRequestId) this.vehicleOptions = [];
})
.finally(() => {
if (requestId === this.vehicleOptionsRequestId) this.vehicleOptionsLoading = false;
});
},
normalizeRow(row) {
const values = { ...row };
values.vehicleType = values.vehicleType || '车辆';
if (values.vehicleNo) {
values.vehicleNo =
values.vehicleType === '船舶'
? values.vehicleNo.trim()
: values.vehicleNo.trim().toUpperCase();
}
if (values.policyNo) {
values.policyNo = values.policyNo.trim();
}
// 非必填项未填写时不要提交 -1 占位值
values.insuredAmount = this.normalizeOptionalAmount(values.insuredAmount);
values.premium = this.normalizeOptionalAmount(values.premium);
values.invoiceNo = this.normalizeOptionalText(values.invoiceNo);
values.invoiceDate = this.normalizeOptionalText(values.invoiceDate);
values.remark = this.normalizeOptionalText(values.remark);
values.policyFile = this.normalizeOptionalText(values.policyFile);
return values;
},
normalizeOptionalAmount(value) {
if (value === undefined || value === null || value === '' || Number(value) === -1) {
return null;
}
return value;
},
normalizeOptionalText(value) {
if (value === undefined || value === null) return null;
const text = String(value).trim();
return !text || text === '-1' ? null : text;
},
normalizeDecimalInput(prop, value) {
const sanitized = String(value ?? '').replace(/[^\d.]/g, '');
const [integerPart, ...decimalParts] = sanitized.split('.');
this.form[prop] = decimalParts.length
? `${integerPart || '0'}.${decimalParts.join('').slice(0, 2)}`
: integerPart;
},
buildQuery(params = {}) {
return normalizeSearchRangeParams({ ...params, ...this.query }, createTimeRangeMap);
},
validateRow(row) {
if (row.startDate && row.endDate && row.endDate < row.startDate) {
this.$message.warning('结束日期不能早于开始日期');
return false;
}
const insuredAmount = this.normalizeOptionalAmount(row.insuredAmount);
const premium = this.normalizeOptionalAmount(row.premium);
if (insuredAmount !== null && Number(insuredAmount) < 0) {
this.$message.warning('保额不能小于 0');
return false;
}
if (premium === null) {
this.$message.warning('请输入保费');
return false;
}
if (Number(premium) < 0) {
this.$message.warning('保费不能小于 0');
return false;
}
if (row.remark && row.remark.length > 200) {
this.$message.warning('备注最多 200 个字');
return false;
}
return true;
},
rowSave(row, done, loading) {
const values = this.normalizeRow(row);
if (!this.validateRow(values)) {
loading();
return;
}
add(values).then(
() => {
this.onLoad(this.page);
this.$message({ type: 'success', message: '操作成功!' });
done();
},
error => {
window.console.log(error);
loading();
}
);
},
rowUpdate(row, index, done, loading) {
const values = this.normalizeRow(row);
if (!this.validateRow(values)) {
loading();
return;
}
update(values).then(
() => {
this.onLoad(this.page);
this.$message({ type: 'success', message: '操作成功!' });
done();
},
error => {
window.console.log(error);
loading();
}
);
},
rowDel(row) {
this.$confirm('确定删除该条数据,删除后不可恢复?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => remove(row.id))
.then(() => {
this.onLoad(this.page);
this.$message({ type: 'success', message: '操作成功!' });
});
},
handleDelete() {
if (this.selectionList.length === 0) {
this.$message.warning('请选择至少一条数据');
return;
}
this.$confirm('确定删除所选数据,删除后不可恢复?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => remove(this.ids))
.then(() => {
this.onLoad(this.page);
this.$message({ type: 'success', message: '操作成功!' });
this.$refs.crud.toggleSelection();
});
},
beforeOpen(done, type) {
this.boxType = type;
if (type === 'add') {
this.vehicleOptions = [];
this.loadVehicleOptions('');
this.form.vehicleType = '车辆';
this.form.insuredAmount = '';
this.form.invoiceNo = '';
this.form.invoiceDate = '';
this.form.remark = '';
this.initOcrTemplateOptions('车辆', true);
}
if (['edit', 'view'].includes(type)) {
this.isDetailLoading = true;
getDetail(this.form.id)
.then(res => {
const detail = res.data.data || {};
this.form = {
...detail,
vehicleType: detail.vehicleType || '车辆',
insuredAmount: this.normalizeOptionalAmount(detail.insuredAmount),
premium: this.normalizeOptionalAmount(detail.premium),
invoiceNo: this.normalizeOptionalText(detail.invoiceNo) || '',
invoiceDate: this.normalizeOptionalText(detail.invoiceDate) || '',
remark: this.normalizeOptionalText(detail.remark) || '',
};
this.vehicleOptions = [];
this.loadVehicleOptions(this.form.vehicleNo || '');
return this.initOcrTemplateOptions(this.form.vehicleType);
})
.finally(() => {
this.isDetailLoading = false;
});
}
done();
},
searchReset() {
this.query = {};
this.onLoad(this.page);
},
searchChange(params, done) {
this.query = this.buildQuery(params);
this.page.currentPage = 1;
this.onLoad(this.page);
done();
},
selectionChange(list) {
this.selectionList = list;
},
selectionClear() {
this.selectionList = [];
this.$refs.crud.toggleSelection();
},
currentChange(currentPage) {
this.page.currentPage = currentPage;
},
sizeChange(pageSize) {
this.page.pageSize = pageSize;
},
refreshChange() {
this.onLoad(this.page, this.query);
},
onLoad(page, params = {}) {
this.loading = true;
const query = this.buildQuery(params);
getList(page.currentPage, page.pageSize, query)
.then(res => {
const data = res.data.data;
this.page.total = data.total;
this.data = data.records;
this.selectionClear();
})
.finally(() => {
this.loading = false;
});
},
handleImport() {
const column = this.findColumn(this.excelOption.column, 'excelFile');
if (!column) {
this.$message.error('导入配置异常,请稍后重试');
return;
}
// 上传前先归一化日期列(2026-9-1 → 2026-09-01),兼容两种格式
column.httpRequest = async (uploadOption, uploadColumn) => {
try {
const normalizedFile = await this.normalizeExcelDates(uploadOption.file);
await handleImportExcel(
this,
{ ...uploadOption, file: normalizedFile },
uploadColumn,
'保险记录',
null,
{ timeout: 60000 }
);
} catch (error) {
this.$message.error(error.message || '导入失败');
if (typeof uploadOption.onError === 'function') {
uploadOption.onError(error);
}
}
};
this.excelBox = true;
},
// 读取 Excel,把指定日期列统一归一化为 YYYY-MM-DD,再写回新文件
async normalizeExcelDates(file) {
const rows = await this.readExcelRows(file);
const dateHeaders = ['开始日期', '结束日期', '开票日期'];
const headerIndex = (rows[0] || []).reduce((map, label, index) => {
const key = String(label || '').trim();
if (key) map[key] = index;
return map;
}, {});
const targetIndexes = dateHeaders
.map(label => headerIndex[label])
.filter(index => index !== undefined);
if (!targetIndexes.length) return file;
rows.forEach((row, rowIndex) => {
if (rowIndex === 0) return;
targetIndexes.forEach(index => {
if (row[index] !== undefined && row[index] !== '') {
const normalized = this.normalizeDateString(String(row[index]));
if (normalized) row[index] = normalized;
}
});
});
return this.writeExcelFile(rows, file.name);
},
async readExcelRows(file) {
const XLSX = await import('xlsx');
const buffer = await file.arrayBuffer();
const workbook = XLSX.read(buffer, { type: 'array', cellDates: false });
const worksheet = workbook.Sheets[workbook.SheetNames[0]];
return XLSX.utils.sheet_to_json(worksheet, {
header: 1,
defval: '',
raw: false,
blankrows: false,
});
},
async writeExcelFile(rows, fileName) {
const XLSX = await import('xlsx');
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, XLSX.utils.aoa_to_sheet(rows), 'Sheet1');
const binary = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
return new File([binary], fileName || 'insurance-record.xlsx', {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
});
},
// 兼容 YYYY-M-D / YYYY-MM-DD / YYYY/M/D / YYYY.年.月.日,非法或已标准格式原样返回 null
normalizeDateString(value) {
const text = String(value || '').trim();
if (!text) return null;
const match = text.match(/^(\d{4})[-/.年](\d{1,2})[-/.月](\d{1,2})日?$/);
if (!match) return null;
const year = Number(match[1]);
const month = Number(match[2]);
const day = Number(match[3]);
const date = new Date(year, month - 1, day);
if (
date.getFullYear() !== year ||
date.getMonth() !== month - 1 ||
date.getDate() !== day
) {
return null;
}
const pad = n => String(n).padStart(2, '0');
return `${year}-${pad(month)}-${pad(day)}`;
},
handleExport() {
this.$confirm('是否导出保险记录数据?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
NProgress.start();
exportBlob(
'/blade-transport/insurance-record/export-insurance-record',
this.buildExportParams(),
{ feedback: true }
)
.then(res => {
downloadXls(res.data, `保险记录${this.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`);
})
.finally(() => {
NProgress.done();
});
});
},
buildExportParams() {
return {
...this.buildQuery(),
ids: this.ids,
exportColumns: JSON.stringify(exportColumns),
[this.website.tokenHeader]: getToken(),
};
},
handleTemplate() {
exportBlob(
'/blade-transport/insurance-record/export-template',
{ exportColumns: JSON.stringify(exportColumns), [this.website.tokenHeader]: getToken() },
{ feedback: true }
).then(res => {
downloadXls(res.data, '保险记录模板.xlsx');
});
},
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 [];
}
},
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;
},
},
};
</script>
<style lang="scss" scoped>
.insurance-record-page {
:deep(.el-table th .cell),
:deep(.el-table td .cell) {
white-space: nowrap;
}
&__vehicle-input {
width: 100%;
}
}
:global(.insurance-record-page__upload) {
display: flex;
align-items: center;
justify-content: space-between;
//min-height: 88px;
padding: 16px 28px 16px 22px;
border: 1px dashed #8b8b8b;
border-radius: 8px;
background: #fff;
}
:global(.insurance-record-page__upload-info) {
display: flex;
align-items: center;
min-width: 0;
}
:global(.insurance-record-page__upload-icon) {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 42px;
height: 42px;
margin-right: 18px;
color: #707070;
}
:global(.insurance-record-page__upload-icon::before),
:global(.insurance-record-page__upload-icon::after) {
position: absolute;
width: 11px;
height: 11px;
content: '';
}
:global(.insurance-record-page__upload-icon::before) {
top: 0;
left: 0;
border-top: 3px solid currentColor;
border-left: 3px solid currentColor;
border-radius: 3px 0 0;
}
:global(.insurance-record-page__upload-icon::after) {
right: 0;
bottom: 0;
border-right: 3px solid currentColor;
border-bottom: 3px solid currentColor;
border-radius: 0 0 3px;
}
:global(.insurance-record-page__upload-card) {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 22px;
border: 3px solid currentColor;
border-radius: 3px;
}
:global(.insurance-record-page__upload-card::before),
:global(.insurance-record-page__upload-card::after) {
position: absolute;
content: '';
}
:global(.insurance-record-page__upload-card::before) {
top: 5px;
left: 7px;
width: 6px;
height: 6px;
border: 2px solid currentColor;
border-radius: 2px;
}
:global(.insurance-record-page__upload-card::after) {
top: 6px;
right: 5px;
width: 6px;
height: 2px;
background: currentColor;
box-shadow: 0 6px 0 currentColor;
}
:global(.insurance-record-page__upload-card span) {
position: absolute;
right: 5px;
bottom: 4px;
width: 8px;
height: 2px;
background: currentColor;
}
:global(.insurance-record-page__upload-text) {
display: flex;
flex-direction: column;
gap: 4px;
color: #303133;
line-height: 1.25;
}
:global(.insurance-record-page__upload-text strong) {
font-weight: 700;
}
:global(.insurance-record-page__upload-text span) {
color: #303133;
font-weight: 500;
}
:global(.insurance-record-page__upload-action) {
flex: 0 0 auto;
margin-left: 18px;
}
:global(.insurance-record-page__upload-action .el-upload) {
display: block;
}
@media (max-width: 768px) {
:global(.insurance-record-page__upload) {
align-items: stretch;
flex-direction: column;
gap: 12px;
padding: 16px;
}
:global(.insurance-record-page__upload-action) {
margin-left: 0;
}
:global(.insurance-record-page__upload-button) {
width: 100%;
}
}
</style>