Merge remote-tracking branch 'origin/master' into feature/bfi-api-rate

# Conflicts:
#	src/views/vehicle/customer-archive.vue
This commit is contained in:
刘泉佳
2026-09-24 03:15:23 +08:00
114 changed files with 10215 additions and 2861 deletions
@@ -0,0 +1,168 @@
<template>
<div ref="page" class="customer-archive-public-view">
<customer-archive ref="archive" />
</div>
</template>
<script>
import CustomerArchive from './customer-archive.vue';
import { postPublicProcessMessage } from '@/api/vehicle/customer-archive';
export default {
name: 'CustomerArchivePublicView',
components: {
CustomerArchive,
},
created() {
this.handleIframeHeight = () => this.sendIframeHeight();
this.handleProcessMessage = event => this.onProcessMessage(event);
document.addEventListener('DOMContentLoaded', this.handleIframeHeight, false);
window.addEventListener('message', this.handleProcessMessage);
},
mounted() {
document.documentElement.classList.add('mk-iframe-page');
document.body.classList.add('mk-iframe-page');
const app = document.getElementById('app');
if (app) app.classList.add('mk-iframe-page');
this.handleIframeHeight();
window.addEventListener('load', this.handleIframeHeight);
this.mkHeightTimers = [300, 800, 1600].map(delay =>
setTimeout(this.handleIframeHeight, delay)
);
if (typeof ResizeObserver === 'undefined') return;
this.mkHeightObserver = new ResizeObserver(() => this.handleIframeHeight());
this.$nextTick(() => {
if (this.$refs.page) this.mkHeightObserver.observe(this.$refs.page);
this.mkHeightObserver.observe(document.body);
});
},
beforeUnmount() {
document.removeEventListener('DOMContentLoaded', this.handleIframeHeight, false);
window.removeEventListener('load', this.handleIframeHeight);
window.removeEventListener('message', this.handleProcessMessage);
(this.mkHeightTimers || []).forEach(timer => clearTimeout(timer));
if (this.mkHeightObserver) {
this.mkHeightObserver.disconnect();
this.mkHeightObserver = null;
}
document.documentElement.classList.remove('mk-iframe-page');
document.body.classList.remove('mk-iframe-page');
const app = document.getElementById('app');
if (app) app.classList.remove('mk-iframe-page');
},
methods: {
sendIframeHeight() {
this.$nextTick(() => {
var tbody = document.body;
var height = tbody.clientHeight;
// 如需动态改变表单高度,可以直接发送此postMessage
window.parent.postMessage({ height: height }, '*');
});
},
onProcessMessage(event) {
const data = event && event.data;
if (!data || typeof data !== 'object') return;
if (data.height && !data.status && !data.type) return;
if (data.type === 'formValues' || data.type === 'afterSubmit') return;
console.log('客商公开页收到流程消息:', data);
const formValues = data.formValues;
if (data.status === 'submit') {
this.submitData(formValues);
} else if (data.status === 'save') {
this.saveData(formValues);
}
if (data.type === 'getFormValues') {
window.parent.postMessage({ type: 'formValues', formData: this.buildFormData() }, '*');
}
},
submitData(lbpmFormValues) {
const formData = this.buildFormData();
this.postFormData('submit', lbpmFormValues, formData);
if (!lbpmFormValues) return;
const parameters = Object.assign({}, lbpmFormValues, {
loginName: this.getLoginName(lbpmFormValues),
formInstanceId: this.getFormId(),
subject: formData.subject,
});
window.parent.postMessage(
{
type: 'afterSubmit',
success: true,
parameters,
},
'*'
);
},
saveData(lbpmFormValues) {
this.postFormData('save', lbpmFormValues, this.buildFormData());
},
postFormData(status, formValues, formData) {
const payload = {
status,
formValues: formValues || {},
formData,
};
console.log('客商公开页提交流程数据:', payload);
postPublicProcessMessage(payload).catch(error => {
console.error('客商公开页提交流程数据失败:', error);
});
},
buildFormData() {
const archive = this.getArchiveForm();
return {
...archive,
subject: archive.fullName || archive.shortName || '',
formInstanceId: this.getFormId(),
};
},
getArchiveForm() {
const archive = this.$refs.archive && this.$refs.archive.archiveForm;
if (!archive) return {};
try {
return JSON.parse(JSON.stringify(archive));
} catch (error) {
return { ...archive };
}
},
getFormId() {
return this.$route.query.id || this.getArchiveForm().id || '';
},
getLoginName(formValues = {}) {
return (
formValues.loginName ||
this.$route.query.loginName ||
this.$route.query.submitIdentity ||
''
);
},
},
};
</script>
<style lang="scss">
html.mk-iframe-page,
html.mk-iframe-page body,
html.mk-iframe-page #app,
html.mk-iframe-page #app.mk-iframe-page {
height: auto !important;
min-height: 100%;
overflow: visible;
}
</style>
<style lang="scss" scoped>
.customer-archive-public-view {
min-height: 100%;
padding: 12px 0 24px;
box-sizing: border-box;
background: #f0f2f5;
:deep(.basic-container) {
padding: 0 12px;
}
:deep(.archive-page-form .archive-form__footer) {
left: 0;
}
}
</style>
+374 -135
View File
@@ -1,5 +1,5 @@
<template>
<basic-container class="customer-archive-page">
<basic-container class="customer-archive-page" :class="{ 'is-public-view': isPublicViewPage }">
<avue-crud
v-if="!isArchivePage"
:option="option"
@@ -150,9 +150,24 @@
>
撤回
</el-link>
<el-link
type="primary"
v-if="
hasPermission('customer_type_re_cert') &&
row.approvalStatus === 'reviewing' &&
row.reviewStatus !== '已完成'
"
@click="openReview(row)"
>
复评
</el-link>
<el-link
type="success"
v-if="hasPermission('customer_archive_approve') && row.approvalStatus === 'reviewing'"
v-if="
hasPermission('customer_archive_approve') &&
row.approvalStatus === 'reviewing' &&
row.reviewStatus === '已完成'
"
@click="handleApprove(row)"
>
审核通过
@@ -547,7 +562,7 @@
<span>{{ formatScoreValue(row.selfScore) }}</span>
</template>
</el-table-column>
<el-table-column label="复评得分" min-width="145" align="center">
<el-table-column v-if="archiveForm.id" label="复评得分" min-width="145" align="center">
<template #default="{ row }">
<span>{{ formatScoreValue(row.reviewScore) }}</span>
</template>
@@ -583,11 +598,18 @@
<span>{{ row.reviewStatus }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="135" fixed="right" align="center">
<el-table-column label="操作" width="160" fixed="right" align="center">
<template #default="{ row }">
<el-link type="primary" @click="openScore(row.__raw, row.__rawIndex)">
详情
</el-link>
<el-link
v-if="showScoreDelete"
type="danger"
@click="deleteScore(row.__rawIndex)"
>
删除
</el-link>
</template>
</el-table-column>
</el-table>
@@ -654,6 +676,7 @@
<el-table-column label="开户人姓名" prop="accountHolderName" min-width="150" />
<el-table-column label="收款账号" prop="bankAccount" min-width="180" />
<el-table-column label="开户行" prop="bankName" min-width="180" />
<el-table-column label="联行号" prop="cnapsCode" min-width="160" />
<el-table-column label="备注" prop="remark" min-width="180" />
<el-table-column
label="操作"
@@ -905,45 +928,12 @@
/>
</section-card>
<el-dialog
<change-record-detail-dialog
v-model="changeRecordDetailVisible"
title="变更记录详情"
append-to-body
destroy-on-close
width="1100px"
top="10px"
class="change-record-detail-dialog"
>
<div v-if="changeRecordDetail" class="change-record-detail-meta">
<span>变更日期:{{ changeRecordDetail.changeTime || '-' }}</span>
<span>变更账号:{{ changeRecordDetail.changeUserName || '-' }}</span>
</div>
<el-table :data="changeRecordDetailRows" border :show-overflow-tooltip="false">
<el-table-column prop="field" label="变更字段" min-width="180" />
<el-table-column
prop="before"
label="变更前"
min-width="360"
class-name="change-record-detail-value"
/>
<el-table-column
prop="after"
label="变更后"
min-width="500"
class-name="change-record-detail-value"
/>
</el-table>
<el-empty
v-if="!changeRecordDetailRows.length"
description="暂无变更内容"
:image-size="60"
/>
<template #footer>
<el-button type="primary" @click="changeRecordDetailVisible = false">关闭</el-button>
</template>
</el-dialog>
:rows="changeRecordDetailRows"
/>
<div class="archive-form__footer">
<div class="archive-form__footer" v-if="!isPublicViewPage">
<!-- 次要:独立页返回 / 只读关闭 / 弹窗取消 -->
<el-button @click="closeArchive">{{ readonly ? '关闭' : '取消' }}</el-button>
<el-button type="primary" plain v-if="!readonly" @click="saveArchive">保存</el-button>
@@ -1052,6 +1042,9 @@
<el-form-item label="开户行" prop="bankName">
<el-input v-model="receiptForm.bankName" maxlength="100" />
</el-form-item>
<el-form-item label="联行号" prop="cnapsCode">
<el-input v-model="receiptForm.cnapsCode" maxlength="32" />
</el-form-item>
<el-form-item label="收款账号" prop="bankAccount">
<el-input v-model="receiptForm.bankAccount" maxlength="50" />
</el-form-item>
@@ -1169,17 +1162,18 @@
<el-input v-model="row.email" maxlength="100" :disabled="readonly" />
</template>
</el-table-column>
<el-table-column label="所属部门" min-width="200">
<el-table-column label="所属部门" min-width="220">
<template #default="{ row }">
<el-input
v-if="!isInternalCustomer"
:model-value="row.deptNames"
disabled
/>
<el-select
<el-cascader
v-else
v-model="row.deptIdList"
multiple
:options="deptTree"
:props="invoiceDeptCascaderProps"
collapse-tags
collapse-tags-tooltip
filterable
@@ -1187,14 +1181,7 @@
:disabled="readonly"
placeholder="请选择所属部门"
@change="() => handleInvoiceContactDeptChange(row)"
>
<el-option
v-for="item in invoiceDeptOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
/>
</template>
</el-table-column>
<el-table-column label="备注" min-width="160">
@@ -1349,7 +1336,25 @@
class="score-detail-table score-detail-table--inline"
max-height="520"
>
<el-table-column label="评分项目" prop="itemName" width="150" align="center" />
<el-table-column
label="评分项目"
width="150"
align="center"
:show-overflow-tooltip="false"
>
<template #default="{ row: detail }">
<el-tooltip
:content="detail.itemName"
placement="top"
effect="dark"
append-to="body"
:show-after="200"
:disabled="!detail.itemName"
>
<span class="score-item-name">{{ detail.itemName }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column label="评分标准" min-width="430">
<template #default="{ row: detail }">
<div class="score-standard-cell">
@@ -1359,6 +1364,7 @@
inputmode="decimal"
:disabled="readonly"
@input="value => handleScoreValueInput(detail, value)"
@blur="handleScoreValueBlur(detail)"
>
<template #append>{{ getScoreRule(detail)?.changeUnit || '' }}</template>
</el-input>
@@ -1388,13 +1394,15 @@
<span>{{ formatScoreValue(getSignedScore(detail, 'selfScore')) }}分</span>
</template>
</el-table-column>
<el-table-column label="复评得分" width="150" align="center">
<el-table-column v-if="!isNewScore" label="复评得分" width="150" align="center">
<template #default="{ row: detail }">
<el-input
v-model="detail.reviewScore"
maxlength="10"
inputmode="decimal"
:disabled="readonly || !hasPermission('customer_type_re_cert')"
:disabled="
(!reviewMode && readonly) || !hasPermission('customer_type_re_cert')
"
@input="value => handleScoreDetailScoreInput(detail, 'reviewScore', value)"
>
<template
@@ -1415,7 +1423,7 @@
<template #default="{ row }">
<span class="score-category-table__name">{{ row.categoryName }}</span>
<span>自评:{{ getSignedScore(row, 'selfScore') }}分</span>
<span>复评:{{ getSignedScore(row, 'reviewScore') }}分</span>
<span v-if="!isNewScore">复评:{{ getSignedScore(row, 'reviewScore') }}分</span>
</template>
</el-table-column>
<el-table-column width="60" align="center">
@@ -1489,12 +1497,15 @@
<div class="score-detail-dialog__total">
<span>总分合计</span>
<span>自评:{{ formatScoreValue(currentScore.selfScore) }}分</span>
<span>复评:{{ formatScoreValue(currentScore.reviewScore) }}分</span>
<span v-if="!isNewScore">复评:{{ formatScoreValue(currentScore.reviewScore) }}分</span>
</div>
<el-button @click="scoreBox = false">取消</el-button>
<el-button type="primary" plain v-if="!readonly" @click="saveScoreDetail">保存</el-button>
<el-button v-if="isNewScore" type="primary" @click="submitSelfScore">提交自评</el-button>
<el-button type="primary" plain v-if="!readonly && !isNewScore" @click="saveScoreDetail">
保存
</el-button>
<el-button
v-if="!readonly && hasPermission('customer_type_re_cert')"
v-if="!isNewScore && (!readonly || reviewMode) && hasPermission('customer_type_re_cert')"
type="primary"
@click="confirmReviewScore"
>
@@ -1512,7 +1523,9 @@ import { ElCascader } from 'element-plus';
import {
getList,
getDetail,
getPublicDetail,
getChangeRecordList,
getPublicChangeRecordList,
submit,
submitApproval,
withdrawApproval,
@@ -1530,6 +1543,7 @@ import {
getDetail as getCreditScoreQuantificationDetail,
} from '@/api/vehicle/credit-score-quantification';
import { getDictionary } from '@/api/system/dictbiz';
import { submitMkApprovalFlow } from '@/utils/mk-approval';
import { getDeptTree } from '@/api/system/dept';
import { getLazyTree } from '@/api/base/region';
import { exportBlob } from '@/api/common';
@@ -1548,6 +1562,7 @@ import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
import SectionCard from '@/components/section-card/main.vue';
import PdfPreview from '@/components/pdf-preview/main.vue';
import ChangeRecordDetailDialog from '@/components/change-record-detail-dialog/main.vue';
const createTimeRangeMap = {
createTimeRange: ['createTimeStart', 'createTimeEnd'],
@@ -1563,11 +1578,18 @@ const qualificationViewerPlugins = [
export default {
components: {
SectionCard,
ChangeRecordDetailDialog,
ArrowRight,
ElImageViewer,
OpenFileViewer,
PdfPreview,
},
props: {
embeddedPublicId: {
type: [String, Number],
default: '',
},
},
data() {
const nonNegativeLabelMap = {
invoiceTaxRate: '开票税点',
@@ -1640,6 +1662,8 @@ export default {
receiptBox: false,
invoiceBox: false,
readonly: false,
reviewMode: false,
listActivated: false,
originalAccessType: '',
activeTab: 'scores',
archiveForm: this.emptyArchive(),
@@ -1800,6 +1824,7 @@ export default {
accountName: [{ required: true, message: '请输入收款单位名称', trigger: 'blur' }],
accountHolderName: [{ required: true, message: '请输入开户人姓名', trigger: 'blur' }],
bankName: [{ required: true, message: '请输入开户行', trigger: 'blur' }],
cnapsCode: [{ required: true, message: '请输入联行号', trigger: 'blur' }],
bankAccount: [{ required: true, message: '请输入收款账号', trigger: 'blur' }],
remark: [{ max: 200, message: '备注最多200个字符', trigger: 'blur' }],
},
@@ -1835,7 +1860,7 @@ export default {
editBtn: false,
selection: true,
dialogClickModal: false,
menuWidth: 280,
menuWidth: 320,
column: [
{
label: '客商编号',
@@ -2019,15 +2044,36 @@ export default {
};
},
created() {
if (this.isPublicViewPage) {
this.readonly = true;
const id = this.embeddedPublicId || this.$route.query.id;
if (!id) {
this.archiveBox = true;
this.$message.error('缺少客商ID');
return;
}
this.openArchive({ id }, true);
return;
}
this.initDeptTree();
this.initRegionOptions();
this.initBusinessDictionaries();
this.initScoreQuantificationOptions();
if (this.isArchivePage) {
const readonly = this.$route.query.view === '1' || this.$route.query.view === 'true';
this.reviewMode = this.$route.query.review === '1';
const readonly =
this.$route.query.view === '1' || this.$route.query.view === 'true' || this.reviewMode;
this.openArchive(this.$route.query.id ? { id: this.$route.query.id } : null, readonly);
}
},
activated() {
if (this.isArchivePage || this.isPublicViewPage) return;
if (!this.listActivated) {
this.listActivated = true;
return;
}
this.onLoad(this.page, this.query);
},
computed: {
...mapGetters(['permission', 'userInfo']),
isAdmin() {
@@ -2043,7 +2089,14 @@ export default {
};
},
isArchivePage() {
return this.$route.path === '/vehicle/customer-archive/form';
return (
this.$route.path === '/vehicle/customer-archive/form' || this.isPublicViewPage
);
},
isPublicViewPage() {
return (
this.$route.path === '/vehicle/customer-archive/public-view' || !!this.embeddedPublicId
);
},
archiveContainer() {
return 'div';
@@ -2069,6 +2122,18 @@ export default {
expandTrigger: 'click',
};
},
// 发票联系信息所属部门:级联多选,可勾选任意层级
invoiceDeptCascaderProps() {
return {
value: 'value',
label: 'label',
children: 'children',
multiple: true,
checkStrictly: true,
emitPath: false,
expandTrigger: 'click',
};
},
// 所属组织级联值:archiveForm.deptId 只存选中的部门 ID(单选),
// 级联控件需要根到选中节点的完整路径,两者在此做双向转换——
// 部门树是异步加载的,依赖 deptTree 可保证树加载完成后回显自动刷新。
@@ -2109,6 +2174,7 @@ export default {
return ids.join(',');
},
dialogTitle() {
if (this.reviewMode) return '客商复评';
if (this.readonly) return '查看客商档案';
return this.archiveForm.id ? '编辑客商档案' : '新增客商档案';
},
@@ -2129,12 +2195,6 @@ export default {
isInternalCustomer() {
return this.archiveForm.customerKind === 'internal';
},
invoiceDeptOptions() {
return this.flattenDept(this.deptTree).map(item => ({
label: item.rawLabel,
value: String(item.value),
}));
},
isTemporaryToFormal() {
return (
Boolean(this.archiveForm.id) &&
@@ -2152,6 +2212,14 @@ export default {
scoreList() {
return this.archiveForm.scores || [];
},
showScoreDelete() {
if (this.readonly) return false;
const status = this.archiveForm.approvalStatus || 'draft';
return status === 'draft' || status === 'rejected';
},
isNewScore() {
return this.scoreRecordIndex < 0 && !this.reviewMode;
},
scorePageCount() {
return Math.max(Math.ceil(this.scoreList.length / this.scorePage.pageSize), 1);
},
@@ -2226,6 +2294,45 @@ export default {
hasPermission(code) {
return this.isAdmin || this.validData(this.permission[code], false);
},
mergeSelectOptions(target, values) {
const list = Array.isArray(values) ? values : values ? [values] : [];
list.forEach(value => {
if (value === undefined || value === null || value === '') return;
if (!this[target].some(item => String(item.value) === String(value))) {
this[target].push({ label: String(value), value });
}
});
},
ensurePublicViewOptions(archive = {}) {
this.mergeSelectOptions('customerNatureOptions', archive.customerNature);
this.mergeSelectOptions('customerTypeOptions', archive.customerType);
this.mergeSelectOptions('businessScopeOptions', archive.businessScope);
this.mergeSelectOptions(
'qualificationTypeOptions',
(this.qualificationFiles || []).map(item => item.type)
);
const deptId = Array.isArray(archive.deptId) ? archive.deptId[0] : archive.deptId;
if (deptId && archive.deptName) {
this.deptTree = [
{
label: archive.deptName,
value: String(deptId),
children: [],
},
];
this.deptOptions = this.flattenDept(this.deptTree);
}
(archive.scores || []).forEach(score => {
if (!score.quantificationId) return;
const id = String(score.quantificationId);
if (!this.scoreQuantificationOptions.some(item => String(item.id) === id)) {
this.scoreQuantificationOptions.push({
id,
name: score.quantificationName || score.name || id,
});
}
});
},
unifiedCreditCodeError(value) {
const code = String(value || '')
.trim()
@@ -2430,7 +2537,8 @@ export default {
loadChangeRecords() {
if (!this.archiveForm.id) return;
const page = this.changeRecordPage;
getChangeRecordList(this.archiveForm.id, page.currentPage, page.pageSize).then(res => {
const requestList = this.isPublicViewPage ? getPublicChangeRecordList : getChangeRecordList;
requestList(this.archiveForm.id, page.currentPage, page.pageSize).then(res => {
const data = res.data.data || {};
page.records = this.replaceNegativeOneWithBlank(data.records || []);
page.total = Number(data.total || 0);
@@ -2788,6 +2896,7 @@ export default {
accountName: '',
accountHolderName: '',
bankName: '',
cnapsCode: '',
bankAccount: '',
registeredPhone: '',
registeredAddress: '',
@@ -3475,7 +3584,17 @@ export default {
this.invoiceForm.contacts.splice(index, 1);
},
handleInvoiceContactDeptChange(row) {
const ids = Array.isArray(row.deptIdList) ? row.deptIdList.map(item => String(item)) : [];
// emitPath=false 时,多选级联值为选中节点 id 数组
const rawList = Array.isArray(row.deptIdList) ? row.deptIdList : [];
const ids = rawList
.map(item => {
if (Array.isArray(item)) {
return item.length ? String(item[item.length - 1]) : '';
}
return item !== undefined && item !== null && item !== '' ? String(item) : '';
})
.filter(Boolean);
row.deptIdList = ids;
row.deptIds = ids.join(',');
row.deptNames = ids.map(id => this.findDeptLabel(id)).filter(Boolean).join('、');
},
@@ -4041,7 +4160,16 @@ export default {
query: { id: row.id, name: '查看客商档案', view: '1' },
});
},
openReview(row) {
this.$router.push({
path: '/vehicle/customer-archive/form',
query: { id: row.id, name: '客商复评', review: '1' },
});
},
closeArchive() {
if (this.isPublicViewPage) return;
this.archiveBox = false;
this.$router.$avueRouter?.closeTag?.();
this.$router.push('/vehicle/customer-archive');
},
openArchive(row, readonly = false) {
@@ -4050,7 +4178,9 @@ export default {
this.resetDetailPagination();
this.resetChangeRecordPage();
if (row && row.id) {
getDetail(row.id).then(res => {
const requestDetail = this.isPublicViewPage ? getPublicDetail : getDetail;
requestDetail(row.id)
.then(res => {
const archive = this.normalizeDetail(res.data.data);
this.archiveForm = readonly ? this.replaceNegativeOneWithBlank(archive) : archive;
this.originalAccessType = this.archiveForm.accessType || '';
@@ -4064,9 +4194,15 @@ export default {
this.qualificationUploadFiles = [];
this.ocrQualificationUploadFiles = [];
this.selectedQualificationFiles = [];
if (this.isPublicViewPage) this.ensurePublicViewOptions(this.archiveForm);
this.archiveBox = true;
this.loadChangeRecords();
});
if (this.reviewMode) this.openReviewScore();
})
.catch(() => {
this.archiveBox = true;
if (this.isPublicViewPage) this.$message.error('客商信息加载失败');
});
return;
}
this.archiveForm = this.emptyArchive();
@@ -4105,14 +4241,15 @@ export default {
this.invoiceForm = this.emptyInvoice();
this.$refs.archiveForm?.clearValidate();
},
// 客商材料不做提交校验,仅在页面上提示未上传项
validateFormalForApproval(archive) {
if (archive.accessType !== 'formal') return true;
const hasCompletedScore = (archive.scores || []).some(
item => item.selfStatus === '已完成' || item.reviewStatus === '已完成' || item.finalScore
);
if (!hasCompletedScore) {
this.$message.warning('正式客商提交审核前必须完成信用评分');
// 客商材料不做提交校验,仅在页面上提示未上传项。提交审批前必须完成自评,且评分记录只能有一条。
validateScoreForApproval(archive) {
const scores = archive.scores || [];
if (scores.length > 1) {
this.$message.warning('评分记录只能有一条');
return false;
}
if (scores.length !== 1 || scores[0].selfStatus !== '已完成') {
this.$message.warning('提交审批前必须完成自评');
return false;
}
return true;
@@ -4164,7 +4301,7 @@ export default {
.filter(item => item.contactName || item.contactPhone);
archive.receiptAccounts = (archive.receiptAccounts || [])
.map(item => this.normalizeReceipt(item))
.filter(item => item.accountName || item.accountHolderName || item.bankAccount);
.filter(item => item.accountName || item.accountHolderName || item.bankAccount || item.cnapsCode);
archive.invoices = (archive.invoices || [])
.map(item => {
const invoice = this.normalizeInvoice(item);
@@ -4230,7 +4367,7 @@ export default {
const archive = this.normalizeArchive();
// 仅提交时落变更记录;保存不记录
archive.recordChange = true;
if (!this.validateFormalForApproval(archive)) return;
if (!this.validateScoreForApproval(archive)) return;
submit(archive).then(res => {
const data = res.data.data;
const id = (data && typeof data === 'object' ? data.id : data) || this.archiveForm.id;
@@ -4240,7 +4377,16 @@ export default {
this.$message({ type: 'success', message: '保存成功,请在列表提交审批' });
return;
}
submitApproval(id).then(() => {
const fullName =
(data && typeof data === 'object' ? data.fullName : '') ||
archive.fullName ||
this.archiveForm.fullName ||
'';
this.submitCustomerApproval(
id,
fullName,
archive.approvalStatus || this.archiveForm.approvalStatus
).then(() => {
this.closeArchive();
this.onLoad(this.page);
this.$message({ type: 'success', message: '提交成功!' });
@@ -4248,9 +4394,32 @@ export default {
});
});
},
/**
* 列表 / 新增 / 编辑提交共用:开启 MK 时提交审核流,并更新客商审批状态
*/
submitCustomerApproval(id, fullName = '', approvalStatus = '') {
return submitMkApprovalFlow({
bizType: 'customer-archive',
formInstanceId: id,
subjectName: fullName,
approvalStatus,
}).then(() => submitApproval(id));
},
openReviewScore() {
const scores = this.archiveForm.scores || [];
if (!scores.length) {
this.$message.warning('暂无评分记录,无法复评');
return;
}
this.$nextTick(() => this.openScore(scores[0], 0));
},
addScore() {
if ((this.archiveForm.scores || []).length) {
this.$message.warning('请勿重复添加');
return;
}
this.scoreRecordIndex = -1;
this.currentScore = this.normalizeScore({});
this.currentScore = this.normalizeScore({ applyCreditLimit: '' });
this.scoreAttachmentFiles = [];
this.expandedScoreCategoryKeys = [];
this.scoreBox = true;
@@ -4332,7 +4501,7 @@ export default {
JSON.stringify(
(item.options || []).map(option => ({
label: option.label || option.optionName || option.value,
value: option.value || option.optionName || option.label,
value: option.value ?? option.optionName ?? option.label,
score: option.score,
changeType: option.changeType,
changeValue: option.changeValue,
@@ -4422,20 +4591,21 @@ export default {
const isScoreOption = item.changeType || item.changeValue || item.changeUnit;
const changeLabel = item.changeType === 'decrease' ? '每减少' : '每增加';
const scoreLabel = item.scoreType === 'subtract' ? '减' : '加';
const rawScore = item.score;
const generatedLabel = isScoreOption
? `${changeLabel}${item.changeValue || ''}${item.changeUnit || ''}${scoreLabel}${
item.score || ''
this.isBlankScore(rawScore) ? '' : rawScore
}分`
: '';
const label = item.label || item.optionName || item.value || generatedLabel;
return {
...item,
label,
value: item.value || item.optionName || item.label || generatedLabel,
value: item.value ?? item.optionName ?? item.label ?? generatedLabel,
score:
item.scoreType === 'subtract' && item.score !== undefined
? -Math.abs(Number(item.score))
: item.score,
item.scoreType === 'subtract' && !this.isBlankScore(rawScore)
? -Math.abs(Number(rawScore))
: rawScore,
};
});
if (Array.isArray(value)) return normalize(value);
@@ -4499,23 +4669,35 @@ export default {
const valid = increase ? input >= base : input <= base;
if (!valid) {
detail.selfScore = '';
this.$message.warning(`输入值${increase ? '不能小于' : '不能大于'}基准数值${base}`);
this.calculateScore(this.currentScore);
return;
}
detail.selfScore = this.getScoreTypeCalculatedScore(detail);
this.calculateScore(this.currentScore);
},
handleScoreValueBlur(detail) {
const normalized = String(detail.scoreInput ?? '').trim();
if (!normalized || normalized === '-' || normalized === '.') return;
const rule = this.getScoreRule(detail);
const base = Number(detail.baseValue);
const input = Number(normalized);
if (!rule || !Number.isFinite(base) || !Number.isFinite(input)) return;
const increase = rule.changeType !== 'decrease';
const valid = increase ? input >= base : input <= base;
if (!valid) {
this.$message.warning(`输入值${increase ? '不能小于' : '不能大于'}基准数值${base}`);
}
},
scoreOptionChange(detail, optionValue) {
const option = this.parseScoreOptions(detail.optionsJson).find(
item => item.value === optionValue
);
const score = Number(option?.score || 0);
detail.selfScore = score;
const rawScore = option?.score;
detail.selfScore = this.isBlankScore(rawScore) ? 0 : Number(rawScore);
this.calculateScore(this.currentScore);
},
handleScoreDetailScoreInput(detail, prop, value) {
const normalized = String(value || '')
const normalized = String(value ?? '')
.replace(/[^\d.]/g, '')
.replace(/^\./, '')
.replace(/(\..*)\./g, '$1')
@@ -4647,8 +4829,7 @@ export default {
sumScore(plusDetails, 'reviewScore') -
sumScore(minusDetails, 'reviewScore');
const hasReviewScore = details.some(
item =>
item.reviewScore !== undefined && item.reviewScore !== null && item.reviewScore !== ''
item => !this.isBlankScore(item.reviewScore)
);
const finalScore = hasReviewScore ? reviewScore : selfScore;
const fullMark = this.getScoreFullMark(details);
@@ -4668,13 +4849,17 @@ export default {
}
return score;
},
isBlankScore(value) {
return value === undefined || value === null || String(value).trim() === '';
},
hasIncompleteBasicScoreDetail(details = []) {
return this.getScoreCategoryDetailsByList(details, 'basic').some(item => {
if (this.isScoreTypeDetail(item)) {
const input = String(item.scoreInput ?? '').trim();
return !input || !Number.isFinite(Number(input)) || item.selfScore === '';
const inputBlank = input === '' || input === '-' || input === '.';
return inputBlank || !Number.isFinite(Number(input)) || this.isBlankScore(item.selfScore);
}
return !item.selectedOption;
return this.isBlankScore(item.selectedOption);
});
},
finishScore(score) {
@@ -4691,10 +4876,13 @@ export default {
score.reviewStatus = '已完成';
this.$message.success('评分已完成');
},
saveScoreDetail() {
saveScoreDetail(successMessage) {
if (!this.validateScoreForm(false)) return;
this.currentScore.attachments = JSON.parse(JSON.stringify(this.scoreAttachmentFiles || []));
this.currentScore.proofAttachments = this.stringifyAttachments(this.scoreAttachmentFiles);
this.currentScore.selfStatus = this.hasIncompleteBasicScoreDetail(this.currentScore.details)
? '未完成'
: '已完成';
this.calculateScore(this.currentScore);
const score = JSON.parse(JSON.stringify(this.currentScore));
if (this.scoreRecordIndex > -1) {
@@ -4706,23 +4894,62 @@ export default {
this.syncArchiveCreditFromScores();
this.activeTab = 'scores';
this.scoreBox = false;
this.saveScoreArchive();
},
saveScoreArchive() {
if (!this.archiveForm.id) {
this.$message.success('评分记录已添加,请保存客商档案后生效');
return;
const pending = this.saveScoreArchive(successMessage);
if (this.reviewMode && successMessage && pending && pending.then) {
pending.then(() => this.closeArchive());
}
submit(this.normalizeArchive()).then(() => {
this.$message.success('评分记录已保存');
return pending;
},
deleteScore(index) {
this.$confirm('确定删除该评分记录?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
if (!Array.isArray(this.archiveForm.scores)) this.archiveForm.scores = [];
this.archiveForm.scores.splice(index, 1);
if (this.scoreRecordIndex === index) {
this.scoreBox = false;
} else if (this.scoreRecordIndex > index) {
this.scoreRecordIndex -= 1;
}
const latestScore = this.getLatestCreditScore(this.archiveForm.scores);
if (latestScore) {
this.syncArchiveCreditFromScores();
} else {
this.archiveForm.customerLevel = '';
this.archiveForm.maxCreditLimit = '';
this.archiveForm.applyCreditLimit = '';
}
this.handleScoreCurrentChange(this.scorePage.currentPage);
this.saveScoreArchive('评分记录已删除', '评分记录已删除,请保存客商档案后生效');
})
.catch(() => {});
},
saveScoreArchive(
successMessage = '评分记录已保存',
draftMessage = '评分记录已添加,请保存客商档案后生效'
) {
if (!this.archiveForm.id) {
this.$message.success(draftMessage);
return Promise.resolve();
}
return submit(this.normalizeArchive()).then(() => {
this.$message.success(successMessage);
});
},
submitSelfScore() {
if (!this.validateScoreForm(true)) return;
this.currentScore.selfStatus = '已完成';
this.currentScore.reviewStatus = '未完成';
this.saveScoreDetail('自评已提交');
},
confirmReviewScore() {
if (!this.validateScoreForm(true)) return;
this.currentScore.selfStatus = '已完成';
this.currentScore.reviewStatus = '已完成';
this.saveScoreDetail();
this.$message.success('复评确认成功');
this.saveScoreDetail('复评确认成功');
},
validateScoreForm(requireComplete) {
if (!this.currentScore.scoreDate) {
@@ -4760,7 +4987,7 @@ export default {
if (
item.reviewScore === undefined ||
item.reviewScore === null ||
item.reviewScore === ''
String(item.reviewScore).trim() === ''
) {
return false;
}
@@ -4777,7 +5004,7 @@ export default {
return false;
}
if (requireComplete && this.hasIncompleteBasicScoreDetail(this.currentScore.details)) {
this.$message.warning('请完整填写基础得分项评分明细');
this.$message.warning('请完整填写所有基础项目后再提交自评');
return false;
}
return true;
@@ -4832,13 +5059,19 @@ export default {
getDetail(row.id).then(res => {
const archive = this.normalizeDetail(res.data.data);
archive.qualificationAttachments = archive.qualificationAttachments || '';
if (!this.validateFormalForApproval(archive)) return;
if (!this.validateScoreForApproval(archive)) return;
this.$confirm('是否提交客商准入审批?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => submitApproval(row.id))
.then(() =>
this.submitCustomerApproval(
row.id,
archive.fullName || row.fullName,
archive.approvalStatus || row.approvalStatus
)
)
.then(() => {
this.onLoad(this.page);
this.$message({ type: 'success', message: '提交成功!' });
@@ -4939,7 +5172,7 @@ export default {
},
selectionClear() {
this.selectionList = [];
this.$refs.crud.toggleSelection();
this.$refs.crud?.toggleSelection();
},
currentChange(currentPage) {
this.page.currentPage = currentPage;
@@ -5332,10 +5565,16 @@ export default {
}
:deep(.el-table__body td) {
//min-height: 112px;
color: #303133;
}
:deep(.score-item-name) {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
:deep(.el-select),
:deep(.el-input),
:deep(.el-select) {
@@ -5370,13 +5609,28 @@ export default {
.invoice-form {
max-width: none;
:deep(.el-input),
:deep(.el-cascader),
:deep(.el-select) {
width: 100%;
}
:deep(.el-cascader .el-input) {
width: 100%;
}
}
.invoice-contact-table {
width: 100%;
:deep(.el-input),
:deep(.el-select) {
:deep(.el-select),
:deep(.el-cascader) {
width: 100%;
}
:deep(.el-cascader .el-input) {
width: 100%;
}
}
@@ -5508,25 +5762,10 @@ export default {
word-break: break-word;
}
:deep(.change-record-detail-dialog .el-dialog__body) {
max-height: 65vh;
overflow: auto;
}
:deep(.change-record-detail-dialog .change-record-detail-value .cell) {
white-space: pre-wrap;
word-break: break-word;
overflow: visible;
text-overflow: clip;
line-height: 1.6;
}
.change-record-detail-meta {
display: flex;
gap: 32px;
margin-bottom: 16px;
color: #606266;
font-size: 14px;
.customer-archive-page.is-public-view {
:deep(.archive-page-form .archive-form__footer) {
left: 0;
}
}
.archive-page-form {
+26
View File
@@ -21,6 +21,15 @@
@on-load="onLoad"
>
<template #menu-left>
<el-button
v-if="hasPermission('equipment_ledger_delete')"
type="danger"
icon="el-icon-delete"
plain
@click="handleDelete"
>
批量删除
</el-button>
<el-button
v-if="hasPermission('equipment_ledger_import')"
type="primary"
@@ -310,6 +319,23 @@ export default {
this.$message.success('操作成功');
});
},
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.success('操作成功');
this.$refs.crud.toggleSelection();
});
},
beforeOpen(done, type) {
this.boxType = type;
if (type === 'add') {
+91 -2
View File
@@ -217,7 +217,7 @@ 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 { openImportDialog } from '@/utils/import-excel';
import { handleImportExcel } from '@/utils/import-excel';
import { getToken } from '@/utils/auth';
import { getUploadHeaders } from '@/utils/upload';
import { normalizeSearchRangeParams } from '@/utils/search-range';
@@ -540,7 +540,96 @@ export default {
});
},
handleImport() {
openImportDialog(this, '保险记录');
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('是否导出保险记录数据?', '提示', {
+3 -3
View File
@@ -516,7 +516,7 @@ export default {
propsHttp: {
res: 'data',
},
tip: '请上传 .xls,.xlsx 标准格式文件',
tip: '请上传 .xls,.xlsx 标准格式文件;日期支持 2026-08-02、2026-8-2、2026/8/2 等写法',
action: '/blade-transport/maintenance-plan/import-maintenance-plan',
},
{
@@ -564,8 +564,8 @@ export default {
this.addressMapPickerVisible = true;
}
},
handleAddressMapConfirm(address) {
this.form.address = address;
handleAddressMapConfirm(payload) {
this.form.address = typeof payload === 'string' ? payload : payload?.address || '';
},
hasPermission(code) {
return this.isAdmin || this.validData(this.permission[code], false);
+3 -3
View File
@@ -489,7 +489,7 @@ export default {
propsHttp: {
res: 'data',
},
tip: '请上传 .xls,.xlsx 标准格式文件',
tip: '请上传 .xls,.xlsx 标准格式文件;日期支持 2026-08-02、2026-8-2、2026/8/2 等写法',
action: '/blade-transport/maintenance-record/import-maintenance-record',
},
{
@@ -545,8 +545,8 @@ export default {
this.addressMapPickerVisible = true;
}
},
handleAddressMapConfirm(address) {
this.form.address = address;
handleAddressMapConfirm(payload) {
this.form.address = typeof payload === 'string' ? payload : payload?.address || '';
},
hasPermission(code) {
return this.isAdmin || this.validData(this.permission[code], false);
+2 -2
View File
@@ -288,8 +288,8 @@ export default {
this.locationMapPickerVisible = true;
}
},
handleLocationMapConfirm(address) {
this.form.location = address;
handleLocationMapConfirm(payload) {
this.form.location = typeof payload === 'string' ? payload : payload?.address || '';
},
hasPermission(code) {
return this.isAdmin || this.validData(this.permission[code], false);