Files
tms-erp-web/src/views/vehicle/customer-archive.vue
T

6790 lines
247 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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="customer-archive-page" :class="{ 'is-public-view': isPublicViewPage }">
<avue-crud
v-if="!isArchivePage"
:option="option"
:table-loading="loading"
:data="data"
v-model:page="page"
v-model="form"
ref="crud"
:permission="permissionList"
@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-plus"
v-if="hasPermission('customer_archive_add')"
@click="openArchivePage()"
>新增
</el-button>
<el-button
type="primary"
icon="el-icon-download"
plain
v-if="hasPermission('customer_archive_export')"
@click="handleExport"
>批量导出
</el-button>
<el-button
type="danger"
icon="el-icon-delete"
plain
v-if="hasPermission('customer_archive_delete')"
@click="handleDelete"
>批量删除
</el-button>
</template>
<template #header>
<el-tabs
v-model="query.expireStatus"
type="card"
class="customer-archive-page__expiry-tabs"
@tab-change="handleExpireChange"
>
<el-tab-pane
v-for="item in expiryTagOptions"
:key="item.value"
:label="item.label"
:name="item.value"
/>
</el-tabs>
</template>
<template #customerCode="{ row }">
<el-button type="primary" link @click="viewArchive(row)">
{{ row.customerCode }}
</el-button>
</template>
<template #customerRisk="{ row }">
<el-tooltip
v-if="customerRowRiskName(row) && customerRowRiskName(row) !== '无风险'"
placement="top"
effect="dark"
popper-class="customer-risk-popper"
>
<template #content>
<div>{{ row.hcsRiskDetail || customerRowRiskName(row) }}</div>
</template>
<el-tag
:type="customerRowRisky(row) ? 'danger' : row.hcsRiskLevel === '1' ? 'warning' : 'success'"
class="status-text"
>
{{ customerRowRiskName(row) }}
</el-tag>
</el-tooltip>
<el-tag
v-else-if="customerRowRiskName(row)"
type="success"
class="status-text"
>
{{ customerRowRiskName(row) }}
</el-tag>
<span v-else>-</span>
</template>
<template #accessType="{ row }">
<el-tag
class="status-text"
:class="
row.accessType === 'formal' ? 'archive-tag--deep-blue' : 'archive-tag--light-blue'
"
>
{{ accessTypeMap[row.accessType] || '临时客商' }}
</el-tag>
</template>
<template #businessEndDate="{ row }">
<span :class="{ 'business-license-expiring': isDateExpiringSoon(row.businessEndDate, row.businessTermType === '长期') }">
{{ formatBusinessLicenseValidPeriod(row) }}
</span>
</template>
<template #nextEvaluateDate="{ row }">
<span :class="{ 'business-license-expiring': isDateExpiringSoon(row.nextEvaluateDate) }">
{{ row.nextEvaluateDate || '-' }}
</span>
</template>
<template #fundUseRate="{ row }">
{{ formatFundUseRate(row) }}
</template>
<template #fundUseRisk="{ row }">
<el-tag v-if="row.fundUseRisk === 'high'" type="danger">
{{ row.fundUseRiskName || '高风险' }}
</el-tag>
<el-tag v-else-if="row.fundUseRisk === 'medium'" type="warning">
{{ row.fundUseRiskName || '中风险' }}
</el-tag>
<span v-else class="fund-use-risk-none">-</span>
</template>
<template #fundUseRisk-header>
<span class="fund-use-risk-header">
资金使用额度风险
<el-tooltip placement="top" effect="dark">
<template #content>
<div>中风险:客户资金额度使用率 ≥ 80%</div>
<div>高风险:客户资金额度使用率 ≥ 90%</div>
</template>
<el-icon class="fund-use-risk-help">
<el-icon-question-filled />
</el-icon>
</el-tooltip>
</span>
</template>
<template #approvalStatus="{ row }">
<el-tag
class="status-text"
:class="{
'archive-tag--deep-blue': row.approvalStatus === 'approved',
'archive-tag--light-blue': row.approvalStatus === 'reviewing',
}"
:type="
['approved', 'reviewing'].includes(row.approvalStatus)
? undefined
: approvalStatusMap[row.approvalStatus]?.type || 'info'
"
>
{{ approvalStatusMap[row.approvalStatus]?.label || '草稿' }}
</el-tag>
</template>
<template #status="{ row }">
<el-tag :type="row.status === 1 ? 'primary' : 'info'" class="status-text">
{{ row.status === 1 ? '启用' : '停用' }}
</el-tag>
</template>
<template #kingdeeStatus="{ row }">
<el-tag v-if="row.kingdeeStatus === 'synced'" type="success">已推送</el-tag>
<el-tag v-else-if="row.kingdeeStatus === 'exist'" type="primary">金蝶已有</el-tag>
<el-tooltip
v-else-if="row.kingdeeStatus === 'failed'"
:content="row.kingdeeFailReason || '推送金蝶失败,请重试'"
placement="top"
>
<el-tag type="danger">推送失败</el-tag>
</el-tooltip>
<el-tag v-else type="info">未推送</el-tag>
</template>
<template #menu="{ row }">
<el-link
type="primary"
v-if="hasPermission('customer_archive_view')"
@click="viewArchive(row)"
>
查看
</el-link>
<el-link
type="primary"
v-if="hasPermission('customer_archive_edit') && row.approvalStatus !== 'reviewing'"
@click="openArchivePage(row)"
>
编辑
</el-link>
<el-link
type="primary"
v-if="
hasPermission('customer_archive_submit') &&
['draft', 'rejected'].includes(row.approvalStatus)
"
@click="handleSubmitApproval(row)"
>
提交
</el-link>
<el-link
type="warning"
v-if="
hasPermission('customer_archive_submit') &&
row.approvalStatus === 'reviewing'
"
@click="handleWithdrawApproval(row)"
>
撤回
</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="primary"
v-if="
mkEnabled &&
hasPermission('customer_archive_approve') &&
['reviewing', 'approved'].includes(row.approvalStatus)
"
@click="handleMkAudit(row)"
>
{{ row.approvalStatus === 'approved' ? '流程' : '审核' }}
</el-link>
<el-link
type="success"
v-if="
!mkEnabled &&
hasPermission('customer_archive_approve') &&
row.approvalStatus === 'reviewing' &&
row.reviewStatus === '已完成'
"
@click="handleApprove(row)"
>
审核通过
</el-link>
<el-link
type="warning"
v-if="
!mkEnabled &&
hasPermission('customer_archive_reject') &&
row.approvalStatus === 'reviewing'
"
@click="handleReject(row)"
>
审核不通过
</el-link>
<el-link
type="primary"
v-if="hasPermission('customer_archive_status')"
@click="handleStatus(row)"
>
{{ row.status === 1 ? '停用' : '启用' }}
</el-link>
<el-link
type="primary"
:disabled="kingdeeSyncingId === row.id"
v-if="
hasPermission('customer_archive_sync') &&
row.approvalStatus === 'approved' &&
!['synced', 'exist'].includes(row.kingdeeStatus)
"
@click="handleSyncKingdee(row)"
>
{{ kingdeeSyncingId === row.id ? '推送中…' : '同步金蝶' }}
</el-link>
<el-link
type="danger"
v-if="hasPermission('customer_archive_delete') && row.approvalStatus === 'draft'"
@click="rowDel(row)"
>
删除
</el-link>
</template>
</avue-crud>
<empty-pagination
v-if="!isArchivePage"
:page="page"
@size-change="sizeChange"
@current-change="currentChange"
@load="onLoad(page, query)"
/>
<component
:is="archiveContainer"
v-if="archiveBox"
v-bind="archiveContainerProps"
@update:model-value="archiveBox = $event"
@closed="resetArchive"
>
<div v-if="isArchivePage" class="archive-page-form__title">
{{ dialogTitle }}
</div>
<el-form
ref="archiveForm"
:model="archiveForm"
:rules="formRules"
label-position="right"
label-width="calc(6em + 24px)"
:disabled="readonly"
class="archive-form dialog-form-label-fixed"
>
<section-card title="基础信息">
<el-row :gutter="18">
<el-col :span="6">
<el-form-item label="客商编号" prop="customerCode">
<el-input
v-model="archiveForm.customerCode"
maxlength="20"
show-word-limit
placeholder="请输入,留空则自动生成"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="供应商编码" prop="supplierCode">
<el-input
v-model="archiveForm.supplierCode"
maxlength="64"
show-word-limit
disabled
placeholder="金蝶供应商编码,系统自动获取"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="客商类型" prop="customerKind">
<el-select
v-model="archiveForm.customerKind"
placeholder="请选择"
filterable
clearable
>
<el-option
v-for="item in customerKindOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="客商性质" prop="customerNature">
<el-select
v-model="archiveForm.customerNature"
placeholder="请输入"
filterable
clearable
>
<el-option
v-for="item in customerNatureOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="是否广西百强" prop="guangxiTop100">
<el-select v-model="archiveForm.guangxiTop100" placeholder="请选择" clearable>
<el-option
v-for="item in guangxiTop100Options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="统一社会信用代码" prop="unifiedCreditCode">
<el-input
v-model="archiveForm.unifiedCreditCode"
placeholder="请输入"
maxlength="18"
show-word-limit
:disabled="kingdeeLocked && Boolean(archiveForm.id)"
@input="
archiveForm.unifiedCreditCode = String(
archiveForm.unifiedCreditCode || ''
).toUpperCase()
"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="客商名称" prop="fullName">
<el-input
v-model="archiveForm.fullName"
placeholder="请输入"
maxlength="100"
show-word-limit
:disabled="kingdeeLocked"
@blur="checkArchiveNameRisk"
/>
<el-tooltip
v-if="archiveNameRisky"
placement="top"
effect="dark"
popper-class="customer-risk-popper"
>
<template #content>
<div>{{ archiveNameRiskTip || '该客商为高风险/黑名单客商' }}</div>
</template>
<div class="archive-name-risk">{{ archiveNameRiskLabel }}</div>
</el-tooltip>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="客商分类" prop="customerType">
<el-select
v-model="archiveForm.customerType"
multiple
placeholder="请选择"
filterable
clearable
>
<el-option
v-for="item in customerTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="企业法人" prop="legalPerson">
<el-input
v-model="archiveForm.legalPerson"
maxlength="10"
show-word-limit
:disabled="kingdeeLocked"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="客户简称" prop="shortName">
<el-input
v-model="archiveForm.shortName"
placeholder="请输入"
maxlength="20"
show-word-limit
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="所在地区" prop="registeredRegionName" required>
<div class="archive-form__address">
<el-cascader
ref="registeredRegionCascader"
v-model="archiveForm.registeredRegionPath"
:options="regionOptions"
:props="regionCascaderProps"
:placeholder="archiveForm.registeredRegionName || '请选择省市区'"
clearable
filterable
@change="handleRegisteredRegionChange"
/>
</div>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="详细地址" prop="registeredDetailAddress" required>
<el-tooltip
:content="archiveForm.registeredDetailAddress"
placement="top"
:disabled="!registeredDetailOverflow"
style="display: block; width: 100%"
>
<el-input
ref="registeredDetailInput"
v-model="archiveForm.registeredDetailAddress"
maxlength="255"
placeholder="请输入详细地址"
@input="checkOverflow('registeredDetailInput', 'registeredDetailOverflow')"
/>
</el-tooltip>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="网络货运平台" prop="networkFreightPlatform">
<el-select
v-model="archiveForm.networkFreightPlatform"
placeholder="请选择"
clearable
>
<el-option
v-for="item in networkFreightPlatformOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item
label="营业期限"
prop="businessEndDate"
:required="archiveForm.businessTermType === '固定期限'"
:rules="businessEndDateRules"
>
<div class="business-term-field">
<el-date-picker
v-model="archiveForm.businessStartDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="开始日期"
/>
<span class="business-term-separator">至</span>
<el-date-picker
v-model="archiveForm.businessEndDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="截止日期"
:disabled="archiveForm.businessTermType === '长期'"
@change="handleBusinessEndDateChange"
/>
<el-checkbox
:model-value="archiveForm.businessTermType === '长期'"
@change="handleNoFixedTermChange"
>
无固定期限
</el-checkbox>
</div>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="注册资金(万元)" prop="registeredCapital">
<el-input
v-model="archiveForm.registeredCapital"
maxlength="18"
:disabled="kingdeeLocked"
@input="value => handleDecimalInput('registeredCapital', value)"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="实缴资本(万元)" prop="paidInCapital">
<el-input
v-model="archiveForm.paidInCapital"
maxlength="18"
:disabled="kingdeeLocked"
@input="value => handleDecimalInput('paidInCapital', value)"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="客户等级" prop="customerLevel">
<el-input v-model="archiveForm.customerLevel" disabled />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="最大资金使用额度(万元)" prop="maxCreditLimit">
<el-input
v-model="archiveForm.maxCreditLimit"
maxlength="18"
disabled
@input="value => handleDecimalInput('maxCreditLimit', value)"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="申请总资金使用额度(万元)" prop="applyCreditLimit">
<el-input v-model="archiveForm.applyCreditLimit" maxlength="18" disabled />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="已使用额度(万元)" prop="usedCreditLimit">
<el-input
v-model="archiveForm.usedCreditLimit"
maxlength="18"
:disabled="readonly || Boolean(archiveForm.id)"
placeholder="新建可录历史已使用额度"
@input="value => handleDecimalInput('usedCreditLimit', value)"
/>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="经营范围" prop="businessScope" class="archive-form__remark">
<el-input
v-model="archiveForm.businessScope"
type="textarea"
:rows="4"
maxlength="2000"
show-word-limit
placeholder="可通过营业执照 OCR 自动识别,也可手动填写"
/>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注" prop="remark" class="archive-form__remark">
<el-input
v-model="archiveForm.remark"
type="textarea"
maxlength="500"
show-word-limit
/>
</el-form-item>
</el-col>
</el-row>
</section-card>
</el-form>
<section-card class="archive-detail-card">
<el-tabs v-model="activeTab" class="archive-tabs" type="card">
<el-tab-pane label="评分表" name="scores">
<div class="tab-table-toolbar" v-if="!readonly">
<el-button type="primary" plain icon="el-icon-plus" @click="addScore"
>添加评分记录</el-button
>
</div>
<el-table :data="scoreTableData" border class="score-list-table">
<el-table-column label="序号" width="90" align="center">
<template #default="{ row }">
<span>{{ row.__index }}</span>
</template>
</el-table-column>
<el-table-column label="评分日期" min-width="150" align="center" sortable>
<template #default="{ row }">
<span>{{ row.scoreDate }}</span>
</template>
</el-table-column>
<el-table-column label="自评得分" min-width="145" align="center">
<template #default="{ row }">
<span>{{ formatScoreValue(row.selfScore) }}</span>
</template>
</el-table-column>
<el-table-column v-if="archiveForm.id" label="复评得分" min-width="145" align="center">
<template #default="{ row }">
<span>{{ formatScoreValue(row.reviewScore) }}</span>
</template>
</el-table-column>
<el-table-column label="信用等级" min-width="150" align="center">
<template #default="{ row }">
<span>{{ row.creditLevel }}</span>
</template>
</el-table-column>
<el-table-column min-width="210" align="center">
<template #header>
<span>最大资金使用额度(万元)</span>
</template>
<template #default="{ row }">
<span>{{ formatScoreValue(row.maxCreditLimit) }}</span>
</template>
</el-table-column>
<el-table-column min-width="220" align="center">
<template #header>
<span>拟申请总资金使用额(万元)</span>
</template>
<template #default="{ row }">
<span>{{ formatScoreValue(row.applyCreditLimit) }}</span>
</template>
</el-table-column>
<el-table-column label="自评状态" min-width="130" align="center">
<template #default="{ row }">
<span>{{ row.selfStatus }}</span>
</template>
</el-table-column>
<el-table-column label="复评状态" min-width="130" align="center">
<template #default="{ row }">
<span>{{ row.reviewStatus }}</span>
</template>
</el-table-column>
<el-table-column label="操作" min-width="220" fixed="right" align="center">
<template #default="{ row }">
<div class="score-list-table__actions">
<el-link type="primary" @click="openScore(row.__raw, row.__rawIndex)">
详情
</el-link>
<el-link
v-if="canReviewScore(row)"
type="primary"
@click="startReviewScore(row.__raw, row.__rawIndex)"
>
复评
</el-link>
<el-link
v-if="showScoreDelete"
type="danger"
@click="deleteScore(row.__rawIndex)"
>
删除
</el-link>
</div>
</template>
</el-table-column>
</el-table>
<empty-pagination
:page="scorePaginationPage"
always-show
@size-change="handleScoreSizeChange"
@current-change="handleScoreCurrentChange"
/>
</el-tab-pane>
<el-tab-pane label="联系人" name="contacts">
<div class="tab-table-toolbar" v-if="!readonly">
<el-button type="primary" plain icon="el-icon-plus" @click="openContact()"
>新增联系人</el-button
>
</div>
<el-table :data="contactTableData" border class="contact-table">
<el-table-column label="序号" prop="__index" width="90" align="center" />
<el-table-column label="联系人姓名" prop="contactName" min-width="150" />
<el-table-column label="联系人电话" prop="contactPhone" min-width="150" />
<el-table-column label="邮箱" prop="email" min-width="180" />
<el-table-column label="联系地址" min-width="240">
<template #default="{ row }">
<span>{{ formatContactAddress(row) || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="备注" prop="remark" min-width="180" />
<el-table-column label="所属分公司/事业部" min-width="170">
<template #default="{ row }">
<span>{{ row.branchName || archiveForm.deptName || '-' }}</span>
</template>
</el-table-column>
<el-table-column
label="操作"
width="130"
fixed="right"
align="center"
v-if="!readonly"
>
<template #default="{ row }">
<el-link type="primary" @click="openContact(row.__raw, row.__rawIndex)">
修改
</el-link>
<el-link type="danger" @click="deleteContact(row.__rawIndex)">删除</el-link>
</template>
</el-table-column>
</el-table>
<empty-pagination
:page="contactPaginationPage"
always-show
@size-change="size => handleDetailSizeChange('contact', size)"
@current-change="page => handleDetailCurrentChange('contact', page)"
/>
</el-tab-pane>
<el-tab-pane label="收款信息" name="receiptAccounts">
<div class="tab-table-toolbar" v-if="!readonly">
<el-button type="primary" plain icon="el-icon-plus" @click="openReceipt()"
>新增收款信息</el-button
>
</div>
<el-table :data="receiptTableData" border class="receipt-table">
<el-table-column label="序号" prop="__index" width="90" align="center" />
<el-table-column label="收款单位名称" prop="accountName" min-width="180" />
<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="操作"
width="130"
fixed="right"
align="center"
v-if="!readonly"
>
<template #default="{ row }">
<el-link type="primary" @click="openReceipt(row.__raw, row.__rawIndex)">
修改
</el-link>
<el-link type="danger" @click="deleteReceipt(row.__rawIndex)">删除</el-link>
</template>
</el-table-column>
</el-table>
<empty-pagination
:page="receiptPaginationPage"
always-show
@size-change="size => handleDetailSizeChange('receipt', size)"
@current-change="page => handleDetailCurrentChange('receipt', page)"
/>
</el-tab-pane>
<el-tab-pane label="发票信息" name="invoices">
<div class="tab-table-toolbar" v-if="!readonly">
<el-button type="primary" plain icon="el-icon-plus" @click="openInvoice()"
>新增发票信息</el-button
>
</div>
<el-table :data="invoiceTableData" border class="invoice-table">
<el-table-column label="序号" prop="__index" width="90" align="center" />
<el-table-column label="企业全称" prop="invoiceTitle" min-width="180" />
<el-table-column label="纳税人识别号" prop="taxNo" min-width="180" />
<el-table-column label="开户行名称" prop="bankName" min-width="180" />
<el-table-column label="银行账号" prop="bankAccount" min-width="180" />
<el-table-column label="注册地址" min-width="220">
<template #default="{ row }">
<span>{{ formatInvoiceRegisteredAddress(row) || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="邮箱" min-width="180">
<template #default="{ row }">
<span>{{ formatInvoiceEmails(row) || '-' }}</span>
</template>
</el-table-column>
<el-table-column
label="操作"
width="130"
fixed="right"
align="center"
v-if="!readonly"
>
<template #default="{ row }">
<el-link type="primary" @click="openInvoice(row.__raw, row.__rawIndex)">
修改
</el-link>
<el-link type="danger" @click="deleteInvoice(row.__rawIndex)">删除</el-link>
</template>
</el-table-column>
</el-table>
<empty-pagination
:page="invoicePaginationPage"
always-show
@size-change="size => handleDetailSizeChange('invoice', size)"
@current-change="page => handleDetailCurrentChange('invoice', page)"
/>
</el-tab-pane>
</el-tabs>
</section-card>
<section-card>
<template #title>
<span>客商材料</span>
<span v-if="missingQualificationText" class="qualification-missing-tip">
{{ missingQualificationText }}
</span>
</template>
<template #extra>
<div class="section-actions">
<el-button type="primary" plain icon="el-icon-download" @click="downloadAttachments"
>批量下载</el-button
>
<vehicle-attachment-upload
v-if="!readonly"
v-model="ocrQualificationUploadFiles"
:multiple="true"
:limit="20"
:max-size="500"
button-text="OCR上传识别"
:show-tip="false"
:show-file-list="false"
@success="handleOcrQualificationUploadSuccess"
/>
</div>
</template>
<el-table
:data="qualificationFiles"
border
@selection-change="handleQualificationSelectionChange"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="序号" type="index" width="90" align="center" />
<el-table-column label="附件类型" min-width="160">
<template #default="{ row }">
<el-select
v-model="row.type"
:disabled="readonly"
filterable
@change="sortQualificationFiles"
>
<el-option
v-for="item in qualificationTypeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
</el-table-column>
<el-table-column
label="文件名称"
min-width="180"
align="left"
show-overflow-tooltip
class-name="is-ellipsis-link"
>
<template #default="{ row }">
<span
class="attachment-file-name"
:class="{ 'is-disabled': !row.url }"
role="button"
tabindex="0"
@click="row.url && previewQualificationFile(row)"
@keydown.enter="row.url && previewQualificationFile(row)"
@keydown.space.prevent="row.url && previewQualificationFile(row)"
>{{ row.originalName || row.name || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="附件描述" min-width="220">
<template #default="{ row }">
<el-input v-model="row.description" :disabled="readonly" maxlength="200" />
</template>
</el-table-column>
<el-table-column label="文件大小" min-width="120">
<template #default="{ row }">
<span>{{ row.size || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="上传人" min-width="120">
<template #default="{ row }">
<span>{{ row.uploadUserName || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="上传时间" min-width="170" sortable>
<template #default="{ row }">
<span>{{ row.uploadTime || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="90" align="center" v-if="!readonly">
<template #default="{ $index }">
<el-link type="danger" @click="qualificationFiles.splice($index, 1)"> 删除 </el-link>
</template>
</el-table-column>
</el-table>
<div v-if="!readonly" class="qualification-upload-bar">
<vehicle-attachment-upload
v-model="qualificationUploadFiles"
:multiple="true"
:limit="20"
accept=".pdf,.bmp,.jpeg,.png,.jpg,.doc,.docx,.ppt,.pptx,.xlsx,.xls,.eml,.msg,.zip"
:max-size="500"
button-text="上传附件"
:show-file-list="false"
:show-uploading="true"
@success="handleQualificationUploadSuccess"
/>
</div>
</section-card>
<el-dialog
v-model="qualificationDocumentPreviewVisible"
:title="qualificationPreviewFile.name || '附件预览'"
append-to-body
destroy-on-close
width="90%"
top="4vh"
>
<pdf-preview
v-if="
qualificationDocumentPreviewVisible &&
qualificationPreviewFile.url &&
qualificationPreviewFile.isPdf
"
:source="qualificationPreviewFile.url"
@error="handleQualificationPreviewError"
/>
<open-file-viewer
v-else-if="qualificationDocumentPreviewVisible && qualificationPreviewFile.url"
:file="qualificationPreviewFile.url"
:file-name="qualificationPreviewFile.name"
:mime-type="qualificationPreviewFile.mimeType"
width="100%"
height="72vh"
fit="contain"
theme="auto"
locale="zh-CN"
:toolbar="qualificationViewerToolbar"
:plugins="qualificationViewerPlugins"
@unsupported="handleQualificationPreviewUnsupported"
@error="handleQualificationPreviewError"
/>
</el-dialog>
<el-image-viewer
v-if="qualificationImagePreviewVisible"
:url-list="qualificationImagePreviewUrls"
:initial-index="qualificationImagePreviewIndex"
@close="qualificationImagePreviewVisible = false"
/>
<section-card v-if="archiveForm.id" title="变更记录">
<el-table :data="changeRecordPage.records" border :show-overflow-tooltip="false">
<el-table-column
label="序号"
type="index"
width="90"
align="center"
:index="changeRecordIndex"
/>
<el-table-column label="变更日期" prop="changeTime" min-width="170" sortable />
<el-table-column label="变更内容" min-width="560">
<template #default="{ row }">
<el-tooltip placement="top" :show-after="200">
<template #content>
<div class="change-record-content-tooltip">{{ formatChangeContent(row) }}</div>
</template>
<span class="change-record-content-cell">{{ formatChangeContent(row) }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column label="变更账号" prop="changeUserName" min-width="160" />
<el-table-column label="操作" width="90" fixed="right" align="center">
<template #default="{ row }">
<el-link type="primary" @click="openChangeRecordDetail(row)">详情</el-link>
</template>
</el-table-column>
</el-table>
<empty-pagination
:page="changeRecordPage"
always-show
@size-change="handleChangeRecordSizeChange"
@current-change="handleChangeRecordCurrentChange"
/>
</section-card>
<change-record-detail-dialog
v-model="changeRecordDetailVisible"
:rows="changeRecordDetailRows"
/>
<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>
<el-button type="primary" v-if="!readonly" @click="saveAndSubmitArchive">提交</el-button>
</div>
</component>
<el-dialog
:title="contactDialogTitle"
append-to-body
v-model="contactBox"
width="680px"
class="contact-dialog"
@closed="resetContact"
@opened="syncContactRegionPath"
>
<section-card>
<el-form
ref="contactForm"
:model="contactForm"
:rules="contactRules"
label-position="right"
label-width="auto"
class="contact-form"
>
<el-form-item label="联系人姓名" prop="contactName" class="contact-form__compact-field">
<el-input v-model="contactForm.contactName" maxlength="30" />
</el-form-item>
<el-form-item label="手机号" prop="contactPhone" class="contact-form__compact-field">
<el-input v-model="contactForm.contactPhone" maxlength="11" />
</el-form-item>
<el-form-item label="邮箱" prop="email" class="contact-form__compact-field">
<el-input v-model="contactForm.email" maxlength="100" />
</el-form-item>
<el-form-item label="所属分公司/事业部" prop="branchName" class="contact-form__compact-field">
<el-cascader
v-model="contactBranchCascaderValue"
:options="deptTree"
:props="deptCascaderProps"
clearable
filterable
placeholder="请选择所属分公司/事业部"
/>
</el-form-item>
<el-form-item label="省市区" prop="regionName" class="contact-form__compact-field">
<el-cascader
ref="contactRegionCascader"
v-model="contactForm.regionPath"
:options="regionOptions"
:props="regionCascaderProps"
:placeholder="contactForm.regionName || '请选择省市区'"
clearable
filterable
@change="handleContactRegionChange"
/>
</el-form-item>
<el-form-item label="详细地址" prop="detailAddress" class="contact-form__compact-field">
<el-input
v-model="contactForm.detailAddress"
maxlength="255"
placeholder="请输入详细地址"
/>
</el-form-item>
<el-form-item label="备注" prop="remark" class="contact-form__compact-field">
<el-input
v-model="contactForm.remark"
type="textarea"
:rows="2"
maxlength="200"
show-word-limit
/>
</el-form-item>
</el-form>
</section-card>
<template #footer>
<div class="contact-dialog__footer-actions">
<el-button @click="contactBox = false">取消</el-button>
<el-button type="primary" plain @click="saveContact">保存</el-button>
</div>
</template>
</el-dialog>
<el-dialog
:title="receiptDialogTitle"
append-to-body
v-model="receiptBox"
width="640px"
class="receipt-dialog"
@closed="resetReceipt"
>
<section-card>
<el-form
ref="receiptForm"
:model="receiptForm"
:rules="receiptRules"
label-position="right"
label-width="120px"
class="receipt-form"
>
<el-form-item label="收款单位名称" prop="accountName">
<el-input v-model="receiptForm.accountName" maxlength="100" />
</el-form-item>
<el-form-item label="开户人姓名" prop="accountHolderName">
<el-input v-model="receiptForm.accountHolderName" maxlength="50" />
</el-form-item>
<el-form-item prop="bankName">
<template #label>
<span class="receipt-bank-label">
开户行
<el-tooltip placement="top" effect="dark" content="请填写银行全称,系统将自动匹配金蝶档案带出联行号">
<el-icon class="receipt-bank-help">
<el-icon-question-filled />
</el-icon>
</el-tooltip>
</span>
</template>
<el-input v-model="receiptForm.bankName" maxlength="100" @blur="fetchReceiptCnapsCode" />
</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>
<el-form-item label="备注" prop="remark">
<el-input
v-model="receiptForm.remark"
type="textarea"
:rows="2"
maxlength="500"
show-word-limit
/>
</el-form-item>
</el-form>
</section-card>
<template #footer>
<div class="receipt-dialog__footer-actions">
<el-button @click="receiptBox = false">取消</el-button>
<el-button type="primary" plain @click="saveReceipt">保存</el-button>
</div>
</template>
</el-dialog>
<el-dialog
:title="invoiceDialogTitle"
append-to-body
v-model="invoiceBox"
width="84%"
class="invoice-dialog"
@closed="resetInvoice"
>
<el-form
ref="invoiceForm"
:model="invoiceForm"
:rules="invoiceRules"
label-position="right"
label-width="120px"
class="invoice-form"
>
<section-card title="发票信息">
<el-row :gutter="34">
<el-col :span="12">
<el-form-item label="纳税人识别号" prop="taxNo">
<el-input
v-model="invoiceForm.taxNo"
maxlength="30"
placeholder="默认客商统一社会信用代码,可编辑"
/>
</el-form-item>
<el-form-item label="注册地址" prop="registeredRegionName">
<el-cascader
ref="invoiceRegisteredRegionCascader"
v-model="invoiceForm.registeredRegionPath"
:options="regionOptions"
:props="regionCascaderProps"
:placeholder="invoiceForm.registeredRegionName || '默认客商地址 可编辑'"
clearable
filterable
@change="handleInvoiceRegisteredRegionChange"
/>
</el-form-item>
<el-form-item label="开户行名称" prop="bankName">
<el-autocomplete
v-model="invoiceForm.bankName"
:fetch-suggestions="queryBankNameSuggestions"
clearable
maxlength="100"
placeholder="请选择或输入开户行名称"
class="invoice-bank-autocomplete"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="企业全称" prop="invoiceTitle">
<el-input
v-model="invoiceForm.invoiceTitle"
maxlength="100"
placeholder="默认客商名称 可编辑"
/>
</el-form-item>
<el-form-item label="详细地址" prop="registeredDetailAddress">
<el-tooltip
:content="invoiceForm.registeredDetailAddress"
placement="top"
:disabled="!invoiceDetailOverflow"
style="display: block; width: 100%"
>
<el-input
ref="invoiceDetailInput"
v-model="invoiceForm.registeredDetailAddress"
maxlength="255"
placeholder="默认客商地址 可编辑"
@input="checkOverflow('invoiceDetailInput', 'invoiceDetailOverflow')"
/>
</el-tooltip>
</el-form-item>
<el-form-item label="银行账号" prop="bankAccount">
<el-input v-model="invoiceForm.bankAccount" maxlength="50" />
</el-form-item>
</el-col>
</el-row>
</section-card>
<section-card>
<template #title>联系信息</template>
<template #extra>
<el-button v-if="!readonly" type="primary" @click="addInvoiceContact">添加</el-button>
</template>
<el-table :data="invoiceForm.contacts || []" border class="invoice-contact-table">
<el-table-column type="index" label="序号" width="70" align="center" />
<el-table-column label="联系人" min-width="120">
<template #default="{ row }">
<el-input v-model="row.contactName" maxlength="30" :disabled="readonly" />
</template>
</el-table-column>
<el-table-column label="联系电话" min-width="140">
<template #default="{ row }">
<el-input v-model="row.contactPhone" maxlength="20" :disabled="readonly" />
</template>
</el-table-column>
<el-table-column label="邮箱地址" min-width="180">
<template #default="{ row }">
<el-input v-model="row.email" maxlength="100" :disabled="readonly" />
</template>
</el-table-column>
<el-table-column label="所属部门" min-width="220">
<template #default="{ row }">
<el-input
v-if="!isInternalCustomer"
:model-value="row.deptNames"
disabled
/>
<el-cascader
v-else
v-model="row.deptIdList"
:options="deptTree"
:props="invoiceDeptCascaderProps"
collapse-tags
collapse-tags-tooltip
filterable
clearable
:disabled="readonly"
placeholder="请选择所属部门"
@change="() => handleInvoiceContactDeptChange(row)"
/>
</template>
</el-table-column>
<el-table-column label="备注" min-width="160">
<template #default="{ row }">
<el-input v-model="row.remark" maxlength="200" :disabled="readonly" />
</template>
</el-table-column>
<el-table-column v-if="!readonly" label="操作" width="80" align="center" fixed="right">
<template #default="{ $index }">
<el-link type="danger" @click="removeInvoiceContact($index)">删除</el-link>
</template>
</el-table-column>
</el-table>
</section-card>
</el-form>
<template #footer>
<div class="invoice-dialog__footer-actions">
<el-button @click="invoiceBox = false">取消</el-button>
<el-button type="primary" plain @click="saveInvoice">保存</el-button>
</div>
</template>
</el-dialog>
<el-dialog
title="评分详情表"
append-to-body
v-model="scoreBox"
width="92%"
class="score-detail-dialog"
@closed="resetScoreDialog"
>
<section-card>
<el-form
ref="scoreForm"
:model="currentScore"
label-position="right"
label-width="88px"
:disabled="readonly"
class="score-detail-form"
>
<el-row :gutter="42">
<el-col :span="12">
<el-form-item label="评分方式" required>
<el-radio-group
v-model="currentScore.evaluateMode"
:disabled="readonly"
@change="handleEvaluateModeChange"
>
<el-radio value="online">线上评估</el-radio>
<el-radio value="offline">线下评估</el-radio>
</el-radio-group>
<span v-if="isOfflineScore" class="score-offline-tip">线下请上传评分表 Excel</span>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="评定日期" required>
<el-date-picker
v-model="currentScore.scoreDate"
type="date"
value-format="YYYY-MM-DD"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="下次评定日期" required>
<el-date-picker
v-model="currentScore.nextEvaluateDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="评分量化表" required>
<el-select
v-model="currentScore.quantificationId"
filterable
clearable
:loading="scoreQuantificationLoading"
placeholder="请选择评分量化表"
@change="handleScoreQuantificationChange"
>
<el-option
v-for="item in scoreQuantificationOptions"
:key="item.id"
:label="item.name"
:value="String(item.id)"
/>
</el-select>
</el-form-item>
</el-col>
<el-col v-if="isOfflineScore" :span="12">
<el-form-item label="评分表">
<div class="score-offline-actions">
<el-button
type="primary"
plain
:disabled="!currentScore.quantificationId || readonly"
@click="downloadScoreTemplate"
>
下载模板
</el-button>
<el-upload
:show-file-list="false"
:disabled="!currentScore.quantificationId || readonly || scoreImporting"
accept=".xls,.xlsx"
:http-request="uploadScoreExcel"
>
<el-button
type="primary"
:loading="scoreImporting"
:disabled="!currentScore.quantificationId || readonly"
>
上传 Excel
</el-button>
</el-upload>
</div>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="信用等级">
<el-input :model-value="formatScoreValue(currentScore.creditLevel)" disabled />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="自评得分" :required="isOfflineScore">
<el-input
v-if="isOfflineScore"
:model-value="formatScoreValue(currentScore.selfScore)"
maxlength="10"
:disabled="readonly"
placeholder="请输入自评得分"
@input="handleOfflineSelfScoreInput"
@blur="recalculateOfflineScore"
>
<template #suffix>分</template>
</el-input>
<el-input v-else :model-value="formatScoreValue(currentScore.selfScore)" disabled>
<template #suffix>分</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="最终得分">
<el-input :model-value="formatScoreValue(currentScore.finalScore)" disabled />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="拟申请总资金使用额度" required>
<el-input
v-model="currentScore.applyCreditLimit"
maxlength="18"
@input="value => handleScoreDecimalInput('applyCreditLimit', value)"
>
<template #suffix>万元</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="最大资金使用额度">
<el-input :model-value="formatScoreValue(currentScore.maxCreditLimit)" disabled>
<template #suffix>万元</template>
</el-input>
</el-form-item>
</el-col>
<el-col v-if="!isOfflineScore" :span="6">
<el-form-item label="临时申请额度">
<el-input
v-model="currentScore.tempApplyCreditLimit"
maxlength="18"
@input="value => handleScoreDecimalInput('tempApplyCreditLimit', value)"
>
<template #suffix>万元</template>
</el-input>
</el-form-item>
</el-col>
<el-col v-if="!isOfflineScore" :span="6">
<el-form-item label="临时申请额度有效期起">
<el-date-picker
v-model="currentScore.tempCreditStartDate"
type="date"
value-format="YYYY-MM-DD"
/>
</el-form-item>
</el-col>
<el-col v-if="!isOfflineScore" :span="6">
<el-form-item label="临时申请额度有效期止">
<el-date-picker
v-model="currentScore.tempCreditEndDate"
type="date"
value-format="YYYY-MM-DD"
/>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注">
<el-input v-model="currentScore.remark" maxlength="200" :rows="2" type="textarea" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<el-table
v-if="!isOfflineScore && currentScore.quantificationId"
ref="scoreCategoryTable"
:data="scoreCategoryRows"
border
:show-header="false"
row-key="categoryCode"
:expand-row-keys="expandedScoreCategoryKeys"
class="score-category-table"
@row-click="toggleScoreCategory"
@expand-change="handleScoreCategoryExpandChange"
>
<el-table-column type="expand" width="1" class-name="score-category-table__expand-column">
<template #default="{ row }">
<el-table
:data="getScoreCategoryDetails(row.categoryCode)"
border
class="score-detail-table score-detail-table--inline"
max-height="520"
>
<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">
<template v-if="isScoreTypeDetail(detail)">
<el-input
:model-value="detail.scoreInput"
inputmode="decimal"
:disabled="readonly"
@input="value => handleScoreValueInput(detail, value)"
@blur="handleScoreValueBlur(detail)"
>
<template #append>{{ getScoreRule(detail)?.changeUnit || '' }}</template>
</el-input>
</template>
<template v-else>
<!-- <div class="score-standard-cell__label">请选择:</div>-->
<el-select
v-model="detail.selectedOption"
filterable
:disabled="readonly"
@change="scoreOptionChange(detail, $event)"
>
<el-option
v-for="item in parseScoreOptions(detail.optionsJson)"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
</div>
</template>
</el-table-column>
<el-table-column label="得分说明" prop="scoreDescription" min-width="160" />
<el-table-column label="自评得分" width="130" align="center">
<template #default="{ row: detail }">
<span>{{ formatScoreValue(getSignedScore(detail, 'selfScore')) }}分</span>
</template>
</el-table-column>
<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="
(!reviewMode && readonly) || !hasPermission('customer_type_re_cert')
"
@input="value => handleScoreDetailScoreInput(detail, 'reviewScore', value)"
>
<template
v-if="isMinusScoreDetail(detail) && Number(detail.reviewScore) > 0"
#prefix
>
-
</template>
<template #suffix>分</template>
</el-input>
</template>
</el-table-column>
</el-table>
</template>
</el-table-column>
<el-table-column label="序号" prop="index" width="50" align="center" />
<el-table-column min-width="560">
<template #default="{ row }">
<span class="score-category-table__name">{{ row.categoryName }}</span>
<span>自评:{{ getSignedScore(row, 'selfScore') }}分</span>
<span v-if="!isNewScore">复评:{{ getSignedScore(row, 'reviewScore') }}分</span>
</template>
</el-table-column>
<el-table-column width="60" align="center">
<template #default="{ row }">
<el-icon
class="score-category-table__arrow"
:class="{ 'is-expanded': isScoreCategoryExpanded(row.categoryCode) }"
>
<ArrowRight />
</el-icon>
</template>
</el-table-column>
</el-table>
</section-card>
<section-card title="证明材料" class="score-material-card">
<template #extra>
<vehicle-attachment-upload
v-if="!readonly"
v-model="scoreAttachmentFiles"
button-text="上传附件"
button-icon="el-icon-upload"
:show-tip="false"
:show-uploading="true"
@success="handleScoreAttachmentUploadSuccess"
/>
</template>
<el-table :data="scoreAttachmentFiles" border class="score-material-table">
<el-table-column label="序号" type="index" width="90" align="center" />
<el-table-column
label="文件名"
min-width="220"
show-overflow-tooltip
class-name="is-ellipsis-link"
>
<template #default="{ row }">
<span>{{ row.originalName || row.name || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="附件描述" min-width="220">
<template #default="{ row }">
<el-input
v-if="!readonly"
v-model="row.description"
placeholder="请输入附件描述"
maxlength="200"
/>
<span v-else>{{ row.description || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="文件大小" min-width="120" align="center">
<template #default="{ row }">
<span>{{ formatAttachmentSize(row.size) || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="上传人" min-width="140" align="center">
<template #default="{ row }">
<span>{{ row.uploadUserName || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="上传时间" min-width="180" align="center" sortable>
<template #default="{ row }">
<span>{{ row.uploadTime || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="120" align="center" v-if="!readonly">
<template #default="{ $index }">
<el-link type="danger" @click="deleteScoreAttachment($index)"> 删除 </el-link>
</template>
</el-table-column>
</el-table>
</section-card>
<template #footer>
<div class="score-detail-dialog__footer-actions">
<div class="score-detail-dialog__total">
<span>总分合计</span>
<span>自评:{{ formatScoreValue(currentScore.selfScore) }}分</span>
<span v-if="!isNewScore">复评:{{ formatScoreValue(currentScore.reviewScore) }}分</span>
</div>
<el-button @click="scoreBox = false">取消</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="!isNewScore && (!readonly || reviewMode) && hasPermission('customer_type_re_cert')"
type="primary"
@click="confirmReviewScore"
>
复评确认
</el-button>
</div>
</template>
</el-dialog>
</basic-container>
</template>
<script>
import { h } from 'vue';
import { ElCascader } from 'element-plus';
import {
getList,
getDetail,
getPublicDetail,
getChangeRecordList,
getPublicChangeRecordList,
submit,
submitApproval,
withdrawApproval,
approve,
reject,
changeStatus,
remove,
getScoreTemplate,
downloadScoreExcelTemplate,
importScoreExcel,
recognizeBusinessLicenseOcr,
queryKingdeeBank,
queryKingdeeBankByNumber,
queryKingdeeCustomer,
querySupplierCode,
syncKingdee,
} from '@/api/vehicle/customer-archive';
import { getRisk, getRiskByName } from '@/api/business/hcs';
import { isKingdeeBlack, isRiskyTag, resolveRiskTagName } from '@/utils/risk-display';
import {
getList as getCreditScoreQuantificationList,
getDetail as getCreditScoreQuantificationDetail,
} from '@/api/vehicle/credit-score-quantification';
import { getDictionary } from '@/api/system/dictbiz';
import { getMKApprovalUrl } from '@/api/system/business-process';
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
import { getDeptTree } from '@/api/system/dept';
import { getLazyTree } from '@/api/base/region';
import { exportBlob } from '@/api/common';
import { downloadFileByUrl, downloadXls } from '@/utils/util';
import {
isExcelResponse,
isBusinessJson,
parseBlobJson,
resolveImportErrorMessage,
} from '@/utils/import-excel';
import { getToken, isAdministrator } from '@/utils/auth';
import { getUploadHeaders } from '@/utils/upload';
import { normalizeSearchRangeParams } from '@/utils/search-range';
import { ArrowRight } from '@element-plus/icons-vue';
import { ElImageViewer, ElLoading } from 'element-plus';
import { OpenFileViewer } from '@open-file-viewer/vue';
import { fallbackPlugin, imagePlugin, officePlugin, textPlugin } from '@open-file-viewer/core';
import '@open-file-viewer/core/style.css';
import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.mjs?url';
import { mapGetters } from 'vuex';
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 customerArchiveListRoute = '/vehicle/customer-archive';
const customerArchiveFormRoute = '/vehicle/customer-archive/form';
const customerArchiveListRefreshKey = 'customer-archive-list-refresh';
/** 客商所属组织固定为顶层组织 */
const FIXED_OWNER_DEPT_NAME = '广西现代物流集团有限公司';
/** 暂定:加分/减分合计上限均为 15 分,超出按 15 分计 */
const HARDCODED_MAX_PLUS_SCORE = 15;
const HARDCODED_MAX_MINUS_SCORE = 15;
const createTimeRangeMap = {
createTimeRange: ['createTimeStart', 'createTimeEnd'],
};
const qualificationViewerPlugins = [
imagePlugin(),
officePlugin({ pdf: { workerSrc: pdfWorkerSrc, useFetchData: true } }),
textPlugin(),
fallbackPlugin(),
];
export default {
components: {
SectionCard,
ChangeRecordDetailDialog,
ArrowRight,
ElImageViewer,
OpenFileViewer,
PdfPreview,
},
props: {
embeddedPublicId: {
type: [String, Number],
default: '',
},
},
data() {
const nonNegativeLabelMap = {
invoiceTaxRate: '开票税点',
registeredCapital: '注册资金',
paidInCapital: '实缴资本',
maxCreditLimit: '最大资金使用额度',
applyCreditLimit: '申请总资金使用额度',
usedCreditLimit: '已使用额度',
};
const validateNonNegative = (rule, value, callback) => {
if (value === undefined || value === null || value === '') {
callback();
} else if (Number(value) < 0) {
callback(new Error(`${nonNegativeLabelMap[rule.field] || '数值'}不能小于0`));
} else {
callback();
}
};
const validateUnifiedCreditCode = (rule, value, callback) => {
const code = String(value || '')
.trim()
.toUpperCase();
if (!code) {
callback();
return;
}
const error = this.unifiedCreditCodeError(code);
if (error) {
callback(new Error(error));
return;
}
callback();
};
return {
form: {},
query: { expireStatus: 'all' },
deptSearchInitialized: false,
loading: true,
scoreImporting: false,
data: [],
archiveNameRisky: false,
archiveNameRiskTip: '',
archiveNameRiskLabel: '',
page: {
pageSize: 10,
pageSizes: [10, 20, 50, 100],
currentPage: 1,
total: 0,
},
selectionList: [],
archiveBox: false,
scoreBox: false,
contactBox: false,
receiptBox: false,
invoiceBox: false,
readonly: false,
reviewMode: false,
listActivated: false,
// 表单页实例标记:创建时按路由落定,避免 keep-alive 缓存后随全局 $route 翻转
isFormPageInstance: false,
originalAccessType: '',
activeTab: 'scores',
archiveForm: this.emptyArchive(),
kingdeeBlocked: false,
kingdeeQueryTimer: null,
kingdeeSyncingId: null,
currentScore: { details: [] },
scoreRecordIndex: -1,
scoreAttachmentFiles: [],
expandedScoreCategoryKeys: [],
scoreQuantificationLoading: false,
scoreQuantificationOptions: [],
scorePage: {
currentPage: 1,
pageSize: 3,
pageSizes: [3, 10, 20, 50],
},
contactPage: {
currentPage: 1,
pageSize: 10,
pageSizes: [10, 20, 50, 100],
},
receiptPage: {
currentPage: 1,
pageSize: 10,
pageSizes: [10, 20, 50, 100],
},
invoicePage: {
currentPage: 1,
pageSize: 10,
pageSizes: [10, 20, 50, 100],
},
changeRecordPage: {
currentPage: 1,
pageSize: 5,
pageSizes: [5],
total: 0,
records: [],
},
changeRecordDetailVisible: false,
changeRecordDetail: null,
changeRecordDetailRows: [],
contactIndex: -1,
contactForm: this.emptyContact(),
receiptIndex: -1,
receiptForm: this.emptyReceiptAccount(),
invoiceIndex: -1,
invoiceForm: this.emptyInvoice(),
registeredDetailOverflow: false,
invoiceDetailOverflow: false,
qualificationFiles: [],
qualificationUploadFiles: [],
ocrQualificationUploadFiles: [],
selectedQualificationFiles: [],
qualificationImagePreviewVisible: false,
qualificationImagePreviewUrls: [],
qualificationImagePreviewIndex: 0,
qualificationDocumentPreviewVisible: false,
qualificationPreviewFile: {},
qualificationViewerPlugins,
qualificationViewerToolbar: {
download: true,
fullscreen: true,
print: true,
rotate: true,
zoom: true,
},
deptTree: [],
deptOptions: [],
regionOptions: [],
customerNatureOptions: [],
customerTypeOptions: [],
customerKindOptions: [
{ label: '外部客商', value: 'external' },
{ label: '内部组织', value: 'internal' },
],
customerKindMap: {
external: '外部客商',
internal: '内部组织',
},
networkFreightPlatformOptions: [
{ label: '是', value: 1 },
{ label: '否', value: 0 },
],
guangxiTop100Options: [
{ label: '是', value: 1 },
{ label: '否', value: 0 },
],
bankListOptions: [],
qualificationTypeOptions: [],
creditLevelOptions: ['A', 'B', 'C', 'D', 'E', 'F'].map(item => ({
label: `${item}级`,
value: `${item}级`,
})),
accessTypeMap: {
temporary: '临时客商',
formal: '正式客商',
},
approvalStatusOptions: [
{ label: '草稿', value: 'draft' },
{ label: '审核中', value: 'reviewing' },
{ label: '审核通过', value: 'approved' },
{ label: '审核不通过', value: 'rejected' },
],
approvalStatusMap: {
draft: { label: '草稿', type: 'info' },
reviewing: { label: '审核中', type: 'warning' },
approved: { label: '审核通过', type: 'success' },
rejected: { label: '审核不通过', type: 'danger' },
},
/** 业务字典 mk_template「开启MK」=1 */
mkEnabled: false,
/** 是否已完成 MK 开关加载,避免提交时误判 */
_mkEnabledResolved: false,
formRules: {
customerCode: [{ max: 20, message: '客商编号最多20个字符', trigger: 'blur' }],
fullName: [{ required: true, message: '请输入客商名称', trigger: 'blur' }],
customerNature: [{ required: true, message: '请选择客商性质', trigger: 'change' }],
unifiedCreditCode: [
{ required: true, message: '请输入统一社会信用代码', trigger: 'blur' },
{
validator: validateUnifiedCreditCode,
trigger: 'blur',
},
],
customerType: [{ required: true, message: '请选择客商分类', trigger: 'change' }],
legalPerson: [{ required: true, message: '请输入企业法人', trigger: 'blur' }],
registeredCapital: [{ validator: validateNonNegative, trigger: 'blur' }],
paidInCapital: [{ validator: validateNonNegative, trigger: 'blur' }],
maxCreditLimit: [{ validator: validateNonNegative, trigger: 'blur' }],
applyCreditLimit: [{ validator: validateNonNegative, trigger: 'blur' }],
usedCreditLimit: [{ validator: validateNonNegative, trigger: 'blur' }],
businessScope: [{ max: 2000, message: '经营范围最多2000个字符', trigger: 'blur' }],
registeredRegionName: [{ required: true, message: '请选择省市区', trigger: 'change' }],
registeredDetailAddress: [
{ required: true, message: '请输入详细地址', trigger: 'blur' },
{ max: 255, message: '详细地址最多255个字符', trigger: 'blur' },
],
remark: [{ max: 500, message: '备注最多500个字符', trigger: 'blur' }],
},
contactRules: {
contactName: [{ required: true, message: '请输入联系人姓名', trigger: 'blur' }],
contactPhone: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ pattern: /^1\d{10}$/, message: '手机号必须为11位手机号', trigger: 'blur' },
],
email: [{ type: 'email', message: '请输入正确的邮箱', trigger: 'blur' }],
branchName: [{ required: true, message: '请选择所属分公司/事业部', trigger: 'change' }],
detailAddress: [{ max: 255, message: '详细地址最多255个字符', trigger: 'blur' }],
remark: [{ max: 200, message: '备注最多200个字符', trigger: 'blur' }],
},
receiptRules: {
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' }],
},
invoiceRules: {
taxNo: [{ required: true, message: '请输入纳税人识别号', trigger: 'blur' }],
invoiceTitle: [{ required: true, message: '请输入企业全称', trigger: 'blur' }],
bankName: [{ required: true, message: '请输入开户行名称', trigger: 'blur' }],
registeredRegionName: [{ required: true, message: '请选择注册地址', trigger: 'change' }],
registeredDetailAddress: [
{ required: true, message: '请输入详细地址', trigger: 'blur' },
{ max: 255, message: '详细地址最多255个字符', trigger: 'blur' },
],
bankAccount: [{ required: true, message: '请输入银行账号', trigger: 'blur' }],
},
option: {
height: 'auto',
calcHeight: 32,
tip: false,
searchBtnText: '查询',
emptyBtnText: '重置',
searchShow: true,
searchMenuSpan: 24,
searchIcon: true,
searchIndex: 4,
searchMenuPosition: 'right',
border: true,
index: true,
indexLabel: '序号',
indexWidth: 90,
addBtn: false,
viewBtn: false,
delBtn: false,
editBtn: false,
selection: true,
dialogClickModal: false,
menuWidth: 400,
column: [
{
label: '客商编号',
prop: 'customerCode',
slot: true,
search: true,
searchOrder: 10,
searchPlaceholder: '请输入',
minWidth: 140,
},
{
label: '供应商编码',
prop: 'supplierCode',
minWidth: 140,
},
{
label: '客商简称',
prop: 'shortName',
search: true,
searchOrder: 2,
searchPlaceholder: '请输入',
minWidth: 150,
},
{
label: '客商名称',
prop: 'fullName',
search: true,
searchOrder: 9,
searchPlaceholder: '请输入',
minWidth: 220,
overHidden: true,
},
{
label: '客商分类',
prop: 'customerType',
search: true,
searchType: 'select',
searchOrder: 7,
searchValue: '',
searchPlaceholder: '请输入',
minWidth: 140,
dicData: [{ label: '全部', value: '' }],
},
{
label: '客商类型',
prop: 'customerKind',
type: 'select',
minWidth: 120,
dicData: [
{ label: '外部客商', value: 'external' },
{ label: '内部组织', value: 'internal' },
],
},
{
label: '客商风险',
prop: 'customerRisk',
slot: true,
search: true,
searchType: 'select',
searchOrder: 4,
searchPlaceholder: '请选择',
minWidth: 150,
// 选项走业务字典 customer_risk(initBusinessDictionaries 加载:dictKey=筛选值,dictValue=名称),未配置时下拉为空
dicData: [],
},
{
label: '客户性质',
prop: 'customerNature',
type: 'select',
search: true,
searchOrder: 5,
searchValue: '',
searchPlaceholder: '请输入',
minWidth: 120,
dicData: [
{ label: '全部', value: '' },
{ label: '有限公司', value: '有限公司' },
{ label: '个体工商户', value: '个体工商户' },
],
},
{
label: '是否广西百强',
prop: 'guangxiTop100',
type: 'select',
minWidth: 130,
dicData: [
{ label: '是', value: 1 },
{ label: '否', value: 0 },
],
formatter: row => {
if (row.guangxiTop100 === 1) return '是';
if (row.guangxiTop100 === 0) return '否';
return '';
},
},
{
label: '统一信用代码',
prop: 'unifiedCreditCode',
search: true,
searchOrder: 3,
searchPlaceholder: '请输入',
minWidth: 190,
},
{
label: '所属组织',
prop: 'deptName',
search: true,
searchOrder: 1,
minWidth: 180,
},
{
label: '准入类型',
prop: 'accessType',
slot: true,
type: 'select',
search: true,
searchOrder: 8,
searchPlaceholder: '请选择',
minWidth: 120,
dicData: [
{ label: '临时', value: 'temporary' },
{ label: '正式', value: 'formal' },
],
},
{
label: '营业执照有效期',
prop: 'businessEndDate',
slot: true,
minWidth: 140,
},
{
label: '下次评定日期',
prop: 'nextEvaluateDate',
slot: true,
minWidth: 130,
},
{
label: '资金使用情况',
prop: 'fundUseRate',
slot: true,
minWidth: 130,
},
{
label: '资金使用额度风险',
prop: 'fundUseRisk',
slot: true,
headerslot: true,
width: 200,
overHidden: false,
},
{
label: '审批状态',
prop: 'approvalStatus',
slot: true,
type: 'select',
search: true,
searchOrder: 4,
searchValue: '',
minWidth: 130,
dicData: [
{ label: '全部', value: '' },
{ label: '草稿', value: 'draft' },
{ label: '审核中', value: 'reviewing' },
{ label: '审核通过', value: 'approved' },
{ label: '审核不通过', value: 'rejected' },
],
},
{ label: '当前节点', prop: 'currentNode', minWidth: 150 },
{ label: '当前处理人', prop: 'currentProcessor', minWidth: 150 },
{
label: '审核通过时间',
prop: 'approvedTime',
sortable: true,
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
minWidth: 170,
},
{
label: '金蝶同步状态',
prop: 'kingdeeStatus',
slot: true,
minWidth: 110,
},
{
label: '状态',
prop: 'status',
slot: true,
type: 'select',
search: true,
searchOrder: 6,
searchValue: '',
minWidth: 100,
dicData: [
{ label: '全部', value: '' },
{ label: '启用', value: 1 },
{ label: '停用', value: 2 },
],
clearable: true,
},
{
label: '创建人',
prop: 'createUserName',
addDisplay: false,
editDisplay: false,
minWidth: 120,
},
{
label: '创建时间',
prop: 'createTime',
sortable: true,
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
addDisplay: false,
editDisplay: false,
minWidth: 170,
},
],
},
};
},
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.isFormPageInstance = this.$route.path === customerArchiveFormRoute;
this.initDeptTree();
this.initRegionOptions();
this.initBusinessDictionaries();
this.initMkEnabled();
this.initScoreQuantificationOptions();
if (this.isArchivePage) {
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.isFormPageInstance || this.isPublicViewPage) return;
if (sessionStorage.getItem(customerArchiveListRefreshKey)) {
sessionStorage.removeItem(customerArchiveListRefreshKey);
this.onLoad(this.page, this.query);
this.listActivated = true;
return;
}
if (!this.listActivated) {
this.listActivated = true;
return;
}
this.onLoad(this.page, this.query);
},
computed: {
...mapGetters(['permission', 'userInfo']),
isAdmin() {
const authority = this.userInfo.authority || '';
return isAdministrator(this.userInfo);
},
kingdeeLocked() {
return this.archiveForm.kingdeeStatus === 'exist';
},
permissionList() {
return {
addBtn: false,
};
},
isArchivePage() {
return this.isFormPageInstance || this.isPublicViewPage;
},
isPublicViewPage() {
return (
this.$route.path === '/vehicle/customer-archive/public-view' || !!this.embeddedPublicId
);
},
archiveContainer() {
return 'div';
},
archiveContainerProps() {
return { class: 'archive-page-form' };
},
regionCascaderProps() {
return {
value: 'id',
label: 'title',
children: 'children',
emitPath: true,
};
},
deptCascaderProps() {
return {
value: 'value',
label: 'label',
children: 'children',
emitPath: true,
checkStrictly: true,
expandTrigger: 'click',
};
},
// 发票联系信息所属部门:级联多选,可勾选任意层级
invoiceDeptCascaderProps() {
return {
value: 'value',
label: 'label',
children: 'children',
multiple: true,
checkStrictly: true,
emitPath: false,
expandTrigger: 'click',
};
},
// 所属组织级联值:archiveForm.deptId 只存选中的部门 ID(单选),
// 级联控件需要根到选中节点的完整路径,两者在此做双向转换——
// 部门树是异步加载的,依赖 deptTree 可保证树加载完成后回显自动刷新。
deptCascaderValue: {
get() {
const ids = Array.isArray(this.archiveForm.deptId)
? this.archiveForm.deptId
: this.archiveForm.deptId
? [this.archiveForm.deptId]
: [];
const currentId = ids.length ? String(ids[ids.length - 1]) : '';
if (!currentId) return [];
return this.findDeptPath(currentId) || [currentId];
},
set(path) {
this.onDeptChange(path);
},
},
// 联系人所属分公司/事业部:表单存名称,级联控件用 id 路径
contactBranchCascaderValue: {
get() {
return this.findDeptPathByName(this.contactForm.branchName);
},
set(path) {
const ids = Array.isArray(path)
? path.filter(item => item !== undefined && item !== null && item !== '')
: [];
const deptId = ids.length ? String(ids[ids.length - 1]) : '';
this.contactForm.branchName = deptId ? this.findDeptLabel(deptId) : '';
this.$refs.contactForm?.validateField('branchName');
},
},
ids() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
dialogTitle() {
if (this.reviewMode) return '客商复评';
if (this.readonly) return '查看客商档案';
return this.archiveForm.id ? '编辑客商档案' : '新增客商档案';
},
businessEndDateRules() {
return this.archiveForm.businessTermType === '固定期限'
? [{ required: true, message: '请选择营业期限截止日', trigger: 'change' }]
: [];
},
contactDialogTitle() {
return this.contactIndex > -1 ? '编辑联系人' : '新增联系人';
},
receiptDialogTitle() {
return this.receiptIndex > -1 ? '编辑收款信息' : '新增收款信息';
},
invoiceDialogTitle() {
return this.invoiceIndex > -1 ? '编辑发票信息' : '新增发票信息';
},
isInternalCustomer() {
return this.archiveForm.customerKind === 'internal';
},
isTemporaryToFormal() {
return (
Boolean(this.archiveForm.id) &&
this.originalAccessType === 'temporary' &&
this.archiveForm.accessType === 'formal'
);
},
missingQualificationText() {
const missing = this.getMissingQualificationTypes();
return missing.length ? `未上传:${missing.join('、')}` : '';
},
uploadHeaders() {
return getUploadHeaders();
},
scoreList() {
return this.archiveForm.scores || [];
},
showScoreDelete() {
if (this.readonly) return false;
const status = this.archiveForm.approvalStatus || 'draft';
// 草稿/驳回可删;审批通过后也允许删除并重建评分表(旧评分逻辑删除保留,新评分审核通过前仍用旧额度)
return status === 'draft' || status === 'rejected' || status === 'approved';
},
isNewScore() {
return this.scoreRecordIndex < 0 && !this.reviewMode;
},
scorePageCount() {
return Math.max(Math.ceil(this.scoreList.length / this.scorePage.pageSize), 1);
},
scorePaginationPage() {
return {
...this.scorePage,
total: this.scoreList.length,
};
},
scoreTableData() {
const start = (this.scorePage.currentPage - 1) * this.scorePage.pageSize;
return this.sortScoresByLatest(this.scoreList)
.slice(start, start + this.scorePage.pageSize)
.map(({ score, index }, pageIndex) => ({
...score,
__raw: score,
__rawIndex: index,
__index: start + pageIndex + 1,
maxCreditLimit: score.maxCreditLimit || '',
}));
},
contactPaginationPage() {
return this.buildDetailPaginationPage('contact');
},
contactTableData() {
return this.buildDetailTableData('contact');
},
receiptPaginationPage() {
return this.buildDetailPaginationPage('receipt');
},
receiptTableData() {
return this.buildDetailTableData('receipt');
},
invoicePaginationPage() {
return this.buildDetailPaginationPage('invoice');
},
invoiceTableData() {
return this.buildDetailTableData('invoice');
},
scoreCategoryRows() {
const categoryMap = [
{ categoryCode: 'basic', categoryName: '基础得分项' },
{ categoryCode: 'plus', categoryName: '加分项目' },
{ categoryCode: 'minus', categoryName: '减分项目' },
];
const rows = categoryMap.map((category, index) => {
const details = this.getScoreCategoryDetails(category.categoryCode);
const sumCategoryScore = prop =>
details.reduce((sum, item) => {
if (this.isBlankScore(item[prop])) return sum;
const value = Number(item[prop]);
return sum + (Number.isFinite(value) ? value : 0);
}, 0);
let selfScore = sumCategoryScore('selfScore');
let reviewScore = sumCategoryScore('reviewScore');
// 暂定:加分/减分合计最高按 15 分计(分类汇总展示封顶后值)
if (category.categoryCode === 'plus') {
selfScore = Math.min(selfScore, HARDCODED_MAX_PLUS_SCORE);
reviewScore = Math.min(reviewScore, HARDCODED_MAX_PLUS_SCORE);
}
if (category.categoryCode === 'minus') {
selfScore = Math.min(selfScore, HARDCODED_MAX_MINUS_SCORE);
reviewScore = Math.min(reviewScore, HARDCODED_MAX_MINUS_SCORE);
}
return {
...category,
index: index + 1,
selfScore: Number(selfScore.toFixed(2)),
reviewScore: Number(reviewScore.toFixed(2)),
};
});
return rows;
},
isOfflineScore() {
return String(this.currentScore?.evaluateMode || 'online') === 'offline';
},
expiryTagOptions() {
return [
{ label: '全部', value: 'all' },
{ label: '30天内到期', value: 'soon' },
{ label: '已过期', value: 'expired' },
];
},
},
watch: {
$route(to, from) {
if (this.isFormPageInstance || this.isPublicViewPage) return;
if (
to.path === customerArchiveListRoute &&
from?.path?.includes('/customer-archive/form')
) {
sessionStorage.removeItem(customerArchiveListRefreshKey);
this.onLoad(this.page, this.query);
}
},
'archiveForm.unifiedCreditCode'(val) {
this.handleKingdeeCreditCodeChange(val);
},
'archiveForm.registeredDetailAddress'() {
this.$nextTick(() => this.checkOverflow('registeredDetailInput', 'registeredDetailOverflow'));
},
'invoiceForm.registeredDetailAddress'() {
this.$nextTick(() => this.checkOverflow('invoiceDetailInput', 'invoiceDetailOverflow'));
},
},
methods: {
markCustomerArchiveListNeedRefresh() {
sessionStorage.setItem(customerArchiveListRefreshKey, '1');
},
hasPermission(code) {
if (this.isAdmin) {
return true;
}
const permission = this.permission || {};
const value = permission[code];
// 不依赖 Avue validData,避免权限值为 true 以外的真值时被误判
return value === true || value === 'true' || value === 1 || value === '1';
},
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(
'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()
.toUpperCase();
const charset = '0123456789ABCDEFGHJKLMNPQRTUWXY';
const weights = [1, 3, 9, 27, 19, 26, 16, 17, 20, 29, 25, 13, 8, 24, 10, 30, 28];
if (code.length !== 18 || ![...code].every(char => charset.includes(char))) {
return '统一社会信用代码必须为18位有效字符';
}
const sum = [...code.slice(0, 17)].reduce(
(total, char, index) => total + charset.indexOf(char) * weights[index],
0
);
const checkCode = charset[(31 - (sum % 31)) % 31];
if (code[17] !== checkCode) {
return '统一社会信用代码校验码错误';
}
return '';
},
resetKingdeeQueryState() {
this.kingdeeBlocked = false;
clearTimeout(this.kingdeeQueryTimer);
this.kingdeeQueryTimer = null;
},
// 新增态下重新输入统一社会信用代码后,此前金蝶回填的数据即失效,重置回填状态并允许对新代码重新查询
resetKingdeeBackfill() {
const form = this.archiveForm;
form.kingdeeStatus = 'none';
form.customerCode = '';
form.supplierCode = '';
form.fullName = '';
form.legalPerson = '';
form.registeredCapital = '';
form.paidInCapital = '';
form.registeredDetailAddress = '';
form.businessScope = '';
form.businessTermType = '固定期限';
form.businessStartDate = '';
form.businessEndDate = '';
},
// 新增态下统一社会信用代码录入完成后自动查询金蝶(A902-1),防抖 500ms;
// 代码一经变化即重新查询(含改回历史代码),不做同码去重
handleKingdeeCreditCodeChange(val) {
this.kingdeeBlocked = false;
if (this.readonly || this.archiveForm.id) return;
this.clearArchiveNameRisk();
if (this.archiveForm.kingdeeStatus === 'exist') {
this.resetKingdeeBackfill();
}
const code = String(val || '')
.trim()
.toUpperCase();
if (code.length !== 18 || this.unifiedCreditCodeError(code)) return;
clearTimeout(this.kingdeeQueryTimer);
this.kingdeeQueryTimer = setTimeout(() => {
this.fetchKingdeeCustomer(code);
this.fetchSupplierCode(code);
}, 500);
},
// 新增态下信用代码录入后即时查询金蝶供应商编码(A901-1)回显,查询失败静默留空,保存/同步时后端兜底落库
async fetchSupplierCode(code) {
try {
const res = await querySupplierCode(code);
// 防止响应返回时用户已修改代码
if (String(this.archiveForm.unifiedCreditCode || '').trim().toUpperCase() !== code) return;
// 后端查不到时 data 为 {}(R 对 null 的空承载兜底),非字符串一律按空处理,避免显示 [object Object]
const supplierCode = res?.data?.data;
this.archiveForm.supplierCode = typeof supplierCode === 'string' ? supplierCode : '';
} catch (error) {
// 忽略查询异常,编码留空
}
},
async fetchKingdeeCustomer(code) {
let customer = null;
try {
const res = await queryKingdeeCustomer(code);
customer = res?.data?.data || null;
} catch (error) {
this.$message.warning('金蝶查询失败,可继续填写,审批通过后系统将自动复查');
return;
}
// 防止响应返回时用户已修改代码
if (String(this.archiveForm.unifiedCreditCode || '').trim().toUpperCase() !== code) return;
// 金蝶不存在(rows 为空,data 为 null 或空对象):静默返回,正常填写,不做提示
if (!customer || !customer.status) return;
if (customer.status === 'C') {
this.applyKingdeeCustomer(customer);
this.$alert(
'金蝶已创建该客户,将获取金蝶的工商信息,相关字段已自动回填并锁定。如信用代码有误,可直接修改,系统将重新查询。',
'提示',
{
confirmButtonText: '确定',
}
);
} else {
this.kingdeeBlocked = true;
this.$alert('金蝶审核中,请稍后重新添加。', '提示', { confirmButtonText: '确定' });
}
},
applyKingdeeCustomer(customer) {
const form = this.archiveForm;
form.kingdeeStatus = 'exist';
// 客商编号回显金蝶客商编码,与后端同步金蝶时的采纳口径保持一致
if (customer.number) form.customerCode = customer.number;
// 工商信息以金蝶为准,直接覆盖(后续字段锁定)
if (customer.name) form.fullName = customer.name;
if (customer.name) this.checkArchiveNameRisk();
if (customer.artificialperson) form.legalPerson = customer.artificialperson;
if (customer.bizpartnerAddress) form.registeredDetailAddress = customer.bizpartnerAddress;
if (customer.regcapital) {
const capital = String(customer.regcapital).replace(/,/g, '').match(/\d+(?:\.\d+)?/);
if (capital) form.registeredCapital = capital[0];
}
if (customer.businessterm) this.applyBusinessTermRecognition(customer.businessterm);
if (customer.businessscope) {
form.businessScope = String(customer.businessscope).trim();
}
if (!form.shortName && customer.simplename) form.shortName = customer.simplename;
this.$nextTick(() => {
[
'unifiedCreditCode',
'fullName',
'legalPerson',
'registeredCapital',
'paidInCapital',
'businessStartDate',
'businessEndDate',
'businessScope',
'registeredDetailAddress',
].forEach(prop => this.$refs.archiveForm?.validateField(prop));
});
},
// 检测输入框文本是否溢出,仅溢出时才允许 tooltip 显示完整地址
checkOverflow(refName, flag) {
const inst = this.$refs[refName];
const el = inst && inst.$el && inst.$el.querySelector('input');
this[flag] = !!(el && el.scrollWidth > el.clientWidth);
},
emptyArchive() {
return {
accessType: 'temporary',
approvalStatus: 'draft',
status: 1,
customerKind: 'external',
deptId: [],
deptIds: '',
deptName: '',
customerType: [],
businessScope: '',
businessTermType: '固定期限',
businessStartDate: '',
businessEndDate: '',
registeredCapital: '',
paidInCapital: '',
usedCreditLimit: '',
networkFreightPlatform: null,
guangxiTop100: null,
registeredRegionPath: [],
registeredRegionName: '',
registeredDetailAddress: '',
registeredAddress: '',
contacts: [],
receiptAccounts: [],
invoices: [],
scores: [],
changeRecords: [],
kingdeeStatus: 'none',
kingdeeFailReason: '',
kingdeeSyncTime: null,
};
},
getDetailList(type) {
const listMap = {
contact: this.archiveForm.contacts,
receipt: this.archiveForm.receiptAccounts,
invoice: this.archiveForm.invoices,
};
return listMap[type] || [];
},
getDetailPage(type) {
const pageMap = {
contact: this.contactPage,
receipt: this.receiptPage,
invoice: this.invoicePage,
};
return pageMap[type];
},
getDetailPageCount(type) {
const page = this.getDetailPage(type);
return Math.max(Math.ceil(this.getDetailList(type).length / page.pageSize), 1);
},
buildDetailPaginationPage(type) {
return {
...this.getDetailPage(type),
total: this.getDetailList(type).length,
};
},
buildDetailTableData(type) {
const page = this.getDetailPage(type);
const start = (page.currentPage - 1) * page.pageSize;
return this.getDetailList(type)
.slice(start, start + page.pageSize)
.map((item, index) => ({
...item,
__raw: item,
__rawIndex: start + index,
__index: start + index + 1,
}));
},
handleDetailCurrentChange(type, currentPage) {
const page = this.getDetailPage(type);
const nextPage = Number(currentPage) || 1;
page.currentPage = Math.min(Math.max(nextPage, 1), this.getDetailPageCount(type));
},
handleDetailSizeChange(type, pageSize) {
const page = this.getDetailPage(type);
page.pageSize = Number(pageSize) || 10;
this.handleDetailCurrentChange(type, 1);
},
resetDetailPagination() {
this.handleScoreCurrentChange(1);
this.handleDetailCurrentChange('contact', 1);
this.handleDetailCurrentChange('receipt', 1);
this.handleDetailCurrentChange('invoice', 1);
},
loadChangeRecords() {
if (!this.archiveForm.id) return;
const page = this.changeRecordPage;
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);
page.currentPage = Number(data.current || page.currentPage);
});
},
handleChangeRecordCurrentChange(currentPage) {
this.changeRecordPage.currentPage = Number(currentPage) || 1;
this.loadChangeRecords();
},
handleChangeRecordSizeChange(pageSize) {
this.changeRecordPage.pageSize = Number(pageSize) || 5;
this.changeRecordPage.currentPage = 1;
this.loadChangeRecords();
},
resetChangeRecordPage() {
this.changeRecordPage.currentPage = 1;
this.changeRecordPage.total = 0;
this.changeRecordPage.records = [];
},
changeRecordIndex(index) {
return (this.changeRecordPage.currentPage - 1) * this.changeRecordPage.pageSize + index + 1;
},
parseChangeData(value) {
if (!value || value === '-1') return {};
if (typeof value === 'object') return this.replaceNegativeOneWithBlank(value);
const text = String(value).trim();
if (!text || text === '-1') return {};
try {
const parsed = JSON.parse(text);
return parsed && typeof parsed === 'object'
? this.replaceNegativeOneWithBlank(parsed)
: { 变更内容: parsed };
} catch (error) {
return text.split(/[;;]/).reduce((data, item) => {
const match = item.trim().match(/^([^::]+)\s*[::]\s*(.*)$/);
if (match) data[match[1].trim()] = match[2].trim();
return data;
}, {});
}
},
getChangeFieldLabel(field) {
const fieldLabelMap = {
accessType: '准入标准',
approvalStatus: '审核状态',
currentNode: '当前节点',
currentProcessor: '当前处理人',
customerCode: '客商编号',
supplierCode: '供应商编码',
shortName: '客商简称',
fullName: '客商名称',
customerType: '客商分类',
customerKind: '客商类型',
customerNature: '客户性质',
guangxiTop100: '是否广西百强',
unifiedCreditCode: '统一信用代码',
deptName: '所属组织',
approvedTime: '审核通过时间',
status: '状态',
businessTermType: '营业期限类型',
businessStartDate: '营业期限开始日',
businessEndDate: '营业期限截止日',
registeredCapital: '注册资金(万元)',
paidInCapital: '实缴资本(万元)',
usedCreditLimit: '已使用额度(万元)',
businessScope: '经营范围',
networkFreightPlatform: '网络货运平台',
registeredRegionName: '注册地址',
registeredDetailAddress: '详细地址',
invoiceType: '发票类型',
qualificationAttachments: '客商材料',
customerAttachments: '客商材料',
客商材料: '客商材料',
};
if (fieldLabelMap[field]) return fieldLabelMap[field];
const column = (this.option.column || []).find(item => item.prop === field);
return column?.label || field;
},
normalizeChangeFieldValue(value) {
if (typeof value !== 'string') return value;
const text = value.trim();
if (!text || (!text.startsWith('[') && !text.startsWith('{') && text !== 'null')) {
return value;
}
try {
return JSON.parse(text);
} catch (error) {
return value;
}
},
isEmptyChangeValue(value) {
const normalized = this.normalizeChangeFieldValue(value);
if (
normalized === undefined ||
normalized === null ||
normalized === '' ||
normalized === '-1'
) {
return true;
}
if (Array.isArray(normalized)) return normalized.length === 0;
if (typeof normalized === 'object') return Object.keys(normalized).length === 0;
return false;
},
normalizeComparableChangeValue(value) {
const normalized = this.normalizeChangeFieldValue(value);
if (this.isEmptyChangeValue(normalized)) return null;
if (typeof normalized === 'number') return Number.isFinite(normalized) ? normalized : null;
if (typeof normalized === 'boolean') return normalized;
if (typeof normalized === 'string') {
const text = normalized.trim();
if (/^-?\d+(\.\d+)?$/.test(text)) {
const number = Number(text);
return Number.isFinite(number) ? number : text;
}
return text;
}
if (Array.isArray(normalized)) {
return normalized.map(item => this.normalizeComparableChangeValue(item));
}
if (normalized && typeof normalized === 'object') {
return Object.keys(normalized)
.sort()
.reduce((result, key) => {
result[key] = this.normalizeComparableChangeValue(normalized[key]);
return result;
}, {});
}
return normalized;
},
isSameChangeValue(beforeValue, afterValue) {
if (beforeValue === afterValue) return true;
if (this.isEmptyChangeValue(beforeValue) && this.isEmptyChangeValue(afterValue)) return true;
return (
JSON.stringify(this.normalizeComparableChangeValue(beforeValue)) ===
JSON.stringify(this.normalizeComparableChangeValue(afterValue))
);
},
formatScoreChangeValue(value) {
const scores = Array.isArray(value) ? value : [value];
const categoryNames = { basic: '基础得分项', plus: '加分项目', minus: '减分项目' };
return scores
.filter(item => item && typeof item === 'object')
.map((score, index) => {
const summary = [
score.scoreDate && `评分日期:${score.scoreDate}`,
score.selfScore !== undefined && score.selfScore !== ''
? `自评得分:${this.formatScoreValue(score.selfScore)}`
: '',
score.reviewScore !== undefined && score.reviewScore !== ''
? `复评得分:${this.formatScoreValue(score.reviewScore)}`
: '',
score.finalScore !== undefined && score.finalScore !== ''
? `最终得分:${this.formatScoreValue(score.finalScore)}`
: '',
score.creditLevel ? `信用等级:${score.creditLevel}` : '',
].filter(Boolean);
const scoreDetails = this.normalizeChangeFieldValue(score.details);
const details = (Array.isArray(scoreDetails) ? scoreDetails : [])
.map(detail => {
const categoryCode = this.getScoreDetailCategoryCode(detail);
const category = categoryNames[categoryCode] || detail.categoryName || '';
const itemName = detail.itemName || '评分项目';
const option = detail.selectedOption || detail.scoreDescription || '';
const points = [
detail.selfScore !== undefined && detail.selfScore !== ''
? `自评${this.formatScoreValue(detail.selfScore)}分`
: '',
detail.reviewScore !== undefined && detail.reviewScore !== ''
? `复评${this.formatScoreValue(detail.reviewScore)}分`
: '',
].filter(Boolean);
if (
!points.length &&
detail.score !== undefined &&
detail.score !== '' &&
detail.score !== -1 &&
detail.score !== '-1'
) {
points.push(`得分${this.formatScoreValue(detail.score)}分`);
}
const label = `${category ? `${category}-` : ''}${itemName}`;
return `${label}${option ? `:${option}` : ''}${
points.length ? `(${points.join(',')})` : ''
}`;
})
.filter(Boolean);
if (details.length) summary.push(`评分明细:${details.join(';')}`);
return summary.length ? summary.join(';') : `第${index + 1}条评分`;
})
.join(';');
},
normalizeScoreChangeEntries(value) {
const normalized = this.normalizeChangeFieldValue(value);
if (Array.isArray(normalized)) return normalized;
return normalized && typeof normalized === 'object' ? [normalized] : [];
},
formatScoreChangeDiff(beforeValue, afterValue) {
const beforeEntries = this.normalizeScoreChangeEntries(beforeValue);
const afterEntries = this.normalizeScoreChangeEntries(afterValue);
const size = Math.max(beforeEntries.length, afterEntries.length);
const fields = [
['scoreDate', '评分日期'],
['selfScore', '自评得分'],
['reviewScore', '复评得分'],
['finalScore', '最终得分'],
['creditLevel', '信用等级'],
['details', '评分明细'],
];
const formatValue = (field, value) => {
if (field === 'details') {
return this.formatScoreChangeValue([{ details: value }]).replace(/^第\d+条评分$/, '空');
}
if (value === undefined || value === null || value === '' || value === '-1') return '空';
return field.endsWith('Score') ? `${this.formatScoreValue(value)}分` : String(value);
};
const before = [];
const after = [];
for (let index = 0; index < size; index += 1) {
const left = beforeEntries[index] || {};
const right = afterEntries[index] || {};
fields.forEach(([field, label]) => {
const leftValue = left[field];
const rightValue = right[field];
if (this.isSameChangeValue(leftValue, rightValue)) return;
const prefix = size > 1 ? `第${index + 1}条-${label}` : label;
before.push(`${prefix}:${formatValue(field, leftValue)}`);
after.push(`${prefix}:${formatValue(field, rightValue)}`);
});
}
return { before: before.join(';') || '空', after: after.join(';') || '空' };
},
formatAttachmentChangeValue(value) {
const attachments = Array.isArray(value)
? value
: value && typeof value === 'object'
? [value]
: this.parseAttachments(value);
const names = attachments
.map(item => {
const fileName =
typeof item === 'string'
? item
: item?.originalName || item?.name || item?.fileName || item?.url || item?.link || '';
const name = String(fileName).trim().split('?')[0].split('/').pop();
try {
return decodeURIComponent(name);
} catch (error) {
return name;
}
})
.filter(Boolean);
return names.length ? names.join('、') : '空';
},
formatChangeFieldValue(field, value) {
value = this.normalizeChangeFieldValue(value);
if (value === undefined || value === null || value === '' || value === '-1') return '空';
const normalizedField =
{
准入类型: 'accessType',
准入标准: 'accessType',
审批状态: 'approvalStatus',
审核状态: 'approvalStatus',
状态: 'status',
客户类型: 'customerType',
客商材料: 'qualificationAttachments',
是否广西百强: 'guangxiTop100',
}[field] || field;
if (['qualificationAttachments', 'customerAttachments'].includes(normalizedField)) {
return this.formatAttachmentChangeValue(value);
}
if (normalizedField === '评分信息' || normalizedField === 'scores') {
return this.formatScoreChangeValue(value) || '空';
}
const valueMap = {
accessType: { temporary: '临时', formal: '正式' },
approvalStatus: {
draft: '草稿',
reviewing: '审核中',
approved: '审核通过',
rejected: '审核不通过',
},
status: { 1: '启用', 2: '停用', 0: '停用' },
customerKind: { external: '外部客商', internal: '内部组织' },
networkFreightPlatform: { 1: '是', 0: '否' },
guangxiTop100: { 1: '是', 0: '否' },
};
if (Array.isArray(value)) {
return value.map(item => this.formatChangeFieldValue(normalizedField, item)).join('、');
}
if (valueMap[normalizedField]?.[value] !== undefined) {
return valueMap[normalizedField][value];
}
if (normalizedField === 'customerType') {
return String(value)
.split(/[,,]/)
.map(item => ({ customer: '客户', carrier: '承运商' }[item.trim()] || item.trim()))
.filter(Boolean)
.join('、');
}
if (typeof value === 'object') return JSON.stringify(value);
return String(value);
},
buildChangeRecordDetailRows(row = {}) {
const beforeData = this.parseChangeData(row.beforeData);
const afterData = this.parseChangeData(row.afterData);
const fields = [...new Set([...Object.keys(beforeData), ...Object.keys(afterData)])];
const rows = fields
.filter(
field =>
field !== '变更内容' && !this.isSameChangeValue(beforeData[field], afterData[field])
)
.map(field => ({
field: this.getChangeFieldLabel(field),
...(this.isScoreChangeField(field)
? this.formatScoreChangeDiff(beforeData[field], afterData[field])
: {
before: this.formatChangeFieldValue(field, beforeData[field]),
after: this.formatChangeFieldValue(field, afterData[field]),
}),
}))
.filter(item => item.before !== item.after);
const content = String(row.changeContent || '').trim();
if (content) {
rows.unshift({ field: '变更内容', before: '空', after: content });
}
return rows;
},
isScoreChangeField(field) {
return ['评分信息', 'scores'].includes(field);
},
openChangeRecordDetail(row) {
this.changeRecordDetail = row;
this.changeRecordDetailRows = this.buildChangeRecordDetailRows(row);
this.changeRecordDetailVisible = true;
},
formatChangeContent(row) {
const rows = this.buildChangeRecordDetailRows(row);
const content = String(row.changeContent || '').trim();
const details = rows
.filter(item => item.field !== '变更内容')
.map(item => `${item.field}:变更前:${item.before} → 变更后:${item.after}`);
return [content, ...details].filter(Boolean).join(';');
},
emptyContact() {
return {
contactName: '',
contactPhone: '',
email: '',
regionPath: [],
regionName: '',
detailAddress: '',
contactAddress: '',
branchName: '',
positionName: '',
isDefault: 0,
remark: '',
};
},
emptyReceiptAccount() {
return {
accountName: '',
accountHolderName: '',
bankName: '',
cnapsCode: '',
bankAccount: '',
registeredPhone: '',
registeredAddress: '',
taxNo: '',
isDefault: 0,
remark: '',
};
},
emptyInvoice() {
return {
invoiceTitle: '',
taxNo: '',
bankName: '',
bankAccount: '',
registeredRegionPath: [],
registeredRegionName: '',
registeredDetailAddress: '',
registeredAddress: '',
contacts: [],
isDefault: 0,
};
},
initDeptTree() {
getDeptTree(this.userInfo.tenantId)
.then(res => {
const deptTree = this.excludeExternalOrganization(res.data.data || []);
this.deptTree = this.normalizeDeptTree(deptTree);
this.deptOptions = this.flattenDept(this.deptTree);
this.setDefaultTopLevelDept();
const deptColumn = this.findColumn(this.option.column, 'deptName');
deptColumn.renderSearch = scope => this.renderDeptSearch(scope);
})
.finally(() => {
this.deptSearchInitialized = true;
this.$nextTick(() => this.onLoad(this.page, this.query));
});
},
initRegionOptions() {
getLazyTree().then(res => {
const regions = this.buildRegionTree(res.data.data || []);
this.regionOptions = this.extractChinaRegionOptions(regions);
});
},
buildRegionTree(regions = []) {
const flatRegions = [];
const collectRegions = list => {
(list || []).forEach(item => {
flatRegions.push({
...item,
children: undefined,
});
if (item.children?.length) collectRegions(item.children);
});
};
collectRegions(regions);
const nodeMap = new Map();
flatRegions.forEach(item => {
const id = item.id === undefined || item.id === null ? item.value : item.id;
if (id === undefined || id === null) return;
nodeMap.set(String(id), {
...item,
id: String(id),
parentId:
item.parentId === undefined || item.parentId === null ? '' : String(item.parentId),
children: [],
});
});
const rootNodes = [];
nodeMap.forEach(node => {
const parent = nodeMap.get(node.parentId);
if (parent && parent !== node) {
parent.children.push(node);
} else {
rootNodes.push(node);
}
});
return rootNodes.map(item => this.normalizeRegionTreeNode(item));
},
normalizeRegionTreeNode(region) {
const children = (region.children || []).map(item => this.normalizeRegionTreeNode(item));
return {
...region,
children: children.length ? children : undefined,
leaf: !children.length,
};
},
isChinaRegion(region = {}) {
const title = String(region.title || region.name || '').trim();
return title === '中国' || title === '中华人民共和国';
},
extractChinaRegionOptions(regions) {
const china = (regions || []).find(item => this.isChinaRegion(item));
if (china?.children?.length) return china.children;
if (regions.length === 1 && regions[0].children?.length) return regions[0].children;
return regions;
},
findRegionOption(options, value) {
for (const item of options || []) {
if (String(item.id) === String(value)) return item;
const child = this.findRegionOption(item.children, value);
if (child) return child;
}
return null;
},
getRegisteredRegionLabels(value, refName = 'registeredRegionCascader') {
const nodes = this.$refs[refName]?.getCheckedNodes?.() || [];
if (nodes.length && nodes[0].pathLabels?.length) {
return nodes[0].pathLabels;
}
return (value || [])
.map(code => this.findRegionOption(this.regionOptions, code)?.title)
.filter(Boolean);
},
handleRegisteredRegionChange(value) {
if (!value || !value.length) {
this.archiveForm.registeredRegionName = '';
this.$refs.archiveForm?.validateField('registeredRegionName');
return;
}
this.$nextTick(() => {
this.archiveForm.registeredRegionName = this.getRegisteredRegionLabels(value).join('');
this.$refs.archiveForm?.validateField('registeredRegionName');
});
},
handleInvoiceRegisteredRegionChange(value) {
if (!value || !value.length) {
this.invoiceForm.registeredRegionName = '';
this.$refs.invoiceForm?.validateField('registeredRegionName');
return;
}
this.$nextTick(() => {
this.invoiceForm.registeredRegionName = this.getRegisteredRegionLabels(
value,
'invoiceRegisteredRegionCascader'
).join('');
this.$refs.invoiceForm?.validateField('registeredRegionName');
});
},
initBusinessDictionaries() {
// 客商风险筛选项走业务字典 customer_risk(dictKey=筛选值 blacklist/high/middle/low,dictValue=名称),字典缺失保留固定兜底项
getDictionary({ code: 'customer_risk' }).then(res => {
const options = (res.data.data || [])
.filter(item => item.dictKey !== '-1')
.map(item => ({ label: item.dictValue, value: item.dictKey }));
if (options.length) {
this.findColumn(this.option.column, 'customerRisk').dicData = options;
}
});
this.loadDictOptions('nature_of_client', 'customerNatureOptions', [], options => {
const customerNatureColumn = this.findColumn(this.option.column, 'customerNature');
customerNatureColumn.dicData = [{ label: '全部', value: '' }, ...options];
});
this.loadDictOptions('customer_type', 'customerTypeOptions', ['客户', '承运商'], options => {
const customerTypeColumn = this.findColumn(this.option.column, 'customerType');
customerTypeColumn.dicData = [{ label: '全部', value: '' }, ...options];
});
this.loadDictOptions('bank_list', 'bankListOptions');
this.loadDictOptions('customer_attachment_types', 'qualificationTypeOptions', [], () => {
this.sortQualificationFiles();
});
},
queryBankNameSuggestions(queryString, callback) {
const query = String(queryString || '')
.trim()
.toLowerCase();
const options = Array.isArray(this.bankListOptions) ? this.bankListOptions : [];
const matched = query
? options.filter(item => String(item.value || '').toLowerCase().includes(query))
: options;
callback(matched.map(item => ({ value: item.value })));
},
initMkEnabled() {
this._mkEnabledResolved = false;
loadMkEnabled()
.then(enabled => {
this.mkEnabled = !!enabled;
this._mkEnabledResolved = true;
})
.catch(() => {
this.mkEnabled = false;
this._mkEnabledResolved = true;
});
},
initScoreQuantificationOptions() {
this.scoreQuantificationLoading = true;
return getCreditScoreQuantificationList(1, 9999, { status: 1 })
.then(res => {
const data = res.data.data || {};
const records = Array.isArray(data) ? data : data.records || [];
this.scoreQuantificationOptions = records
.filter(item => Number(item.status) === 1)
.map(item => ({
id: String(item.id),
name: item.name || item.configName || item.title || item.id,
}));
})
.finally(() => {
this.scoreQuantificationLoading = false;
});
},
ensureScoreQuantificationOption(quantificationId) {
if (!quantificationId) return Promise.resolve();
const id = String(quantificationId);
if (this.scoreQuantificationOptions.some(item => String(item.id) === id)) {
return Promise.resolve();
}
return getCreditScoreQuantificationDetail(quantificationId).then(res => {
const item = res.data.data || {};
if (!item.id) return;
this.scoreQuantificationOptions.push({
id: String(item.id),
name: item.name || item.configName || item.title || String(item.id),
});
});
},
loadDictOptions(code, target, fallback = [], callback) {
getDictionary({ code }).then(res => {
const options = (res.data.data || []).map(item => ({
label: item.dictValue,
value: item.dictValue,
}));
fallback.forEach(value => {
if (!options.some(item => item.value === value)) {
options.push({ label: value, value });
}
});
this[target] = options;
if (callback) callback(options);
});
},
normalizeDeptTree(tree = []) {
return (tree || []).map(item => {
const label = item.label || item.title || item.deptName || item.name || '';
const value = String(item.value || item.id || '');
return {
...item,
label,
value,
children: this.normalizeDeptTree(item.children || []),
};
});
},
excludeExternalOrganization(tree = []) {
return tree.reduce((result, item) => {
const deptName = item.label || item.title || item.deptName || item.name || '';
if (String(deptName).trim() === '外部组织') return result;
result.push({
...item,
children: this.excludeExternalOrganization(item.children || []),
});
return result;
}, []);
},
flattenDept(tree, level = 0) {
const result = [];
tree.forEach(item => {
result.push({
label: `${' '.repeat(level)}${item.label || item.title || item.deptName || item.name}`,
value: item.value || item.id,
rawLabel: item.label || item.title || item.deptName || item.name,
});
if (item.children && item.children.length) {
result.push(...this.flattenDept(item.children, level + 1));
}
});
return result;
},
findTopLevelDeptByName(name) {
const targetName = String(name || '').trim();
if (!targetName) {
return null;
}
return (
(this.deptTree || []).find(item => {
const label = String(item.label || item.title || item.deptName || item.name || '').trim();
return label === targetName;
}) || null
);
},
/**
* 客商所属组织固定为顶层「广西现代物流集团有限公司」,表单不可编辑。
*/
applyFixedOwnerDept() {
if (!this.isArchivePage || this.readonly) {
return;
}
const fixedDept =
this.findTopLevelDeptByName(FIXED_OWNER_DEPT_NAME) || (this.deptTree || [])[0] || null;
if (!fixedDept) {
return;
}
this.onDeptChange([fixedDept.value]);
},
setDefaultTopLevelDept() {
this.applyFixedOwnerDept();
},
onDeptChange(value) {
// 级联单选:value 为根到选中节点的 ID 路径,取末级作为客商所属组织
const path = Array.isArray(value)
? value.filter(item => item !== undefined && item !== null && item !== '')
: [];
const deptId = path.length ? String(path[path.length - 1]) : '';
this.archiveForm.deptId = deptId ? [deptId] : [];
this.archiveForm.deptIds = deptId;
this.archiveForm.deptName = deptId ? this.findDeptLabel(deptId) : '';
this.$refs.archiveForm?.validateField('deptName');
},
findDeptPath(id, tree = this.deptTree, parents = []) {
for (const node of tree || []) {
const path = [...parents, String(node.value)];
if (String(node.value) === String(id)) return path;
const children = node.children || [];
if (children.length) {
const matched = this.findDeptPath(id, children, path);
if (matched) return matched;
}
}
return null;
},
findDeptLabel(id, tree = this.deptTree) {
for (const node of tree || []) {
if (String(node.value) === String(id)) return node.label;
const matched = this.findDeptLabel(id, node.children || []);
if (matched) return matched;
}
return '';
},
findDeptPathByName(name, tree = this.deptTree, parents = []) {
const target = String(name || '').trim();
if (!target) return [];
for (const node of tree || []) {
const path = [...parents, String(node.value)];
if (String(node.label || '').trim() === target) return path;
const matched = this.findDeptPathByName(name, node.children || [], path);
if (matched.length) return matched;
}
return [];
},
getCurrentOrganizationName() {
const currentDeptId = String(this.userInfo.deptId || this.userInfo.dept_id || '')
.split(',')
.map(item => item.trim())
.filter(Boolean)[0];
if (currentDeptId) {
const label = this.findDeptLabel(currentDeptId);
if (label) return label;
}
return this.userInfo.deptName || this.userInfo.dept_name || '';
},
findRegionPathByName(options = [], regionName = '', parents = []) {
const target = String(regionName || '').replace(/\s+/g, '');
if (!target) return [];
for (const item of options || []) {
const nextParents = [...parents, item];
const nextName = nextParents.map(region => region.title || region.name || '').join('');
if (nextName === target) {
return nextParents.map(region => region.id);
}
const childPath = this.findRegionPathByName(item.children, target, nextParents);
if (childPath.length) return childPath;
}
return [];
},
getContactRegionLabels(value) {
const nodes = this.$refs.contactRegionCascader?.getCheckedNodes?.() || [];
if (nodes.length && nodes[0].pathLabels?.length) {
return nodes[0].pathLabels;
}
return (value || [])
.map(code => this.findRegionOption(this.regionOptions, code)?.title)
.filter(Boolean);
},
handleContactRegionChange(value) {
if (!value || !value.length) {
this.contactForm.regionName = '';
return;
}
this.$nextTick(() => {
this.contactForm.regionName = this.getContactRegionLabels(value).join('');
});
},
syncContactRegionPath() {
if (!this.contactForm.regionName || this.contactForm.regionPath?.length) return;
const path = this.findRegionPathByName(this.regionOptions, this.contactForm.regionName);
if (path.length) {
this.contactForm.regionPath = path;
}
},
renderDeptSearch(scope) {
// 搜索区所属组织:单选级联(与新增/编辑表单一致)
// 级联返回的是根到选中节点的 ID 路径数组,提交时取末级 ID 转部门名称
return h(ElCascader, {
modelValue: Array.isArray(scope.row?.deptName) ? scope.row.deptName : [],
'onUpdate:modelValue': value => {
// 级联单选:modelValue 必须是 ID 路径数组,否则控件内部状态会错乱导致无法再次选中
if (scope.row) scope.row.deptName = Array.isArray(value) ? value : [];
},
options: this.deptTree,
props: {
label: 'label',
value: 'value',
children: 'children',
checkStrictly: true,
expandTrigger: 'click',
},
filterable: true,
clearable: true,
style: 'width: 100%',
placeholder: '请选择 所属组织',
});
},
handleNoFixedTermChange(checked) {
this.archiveForm.businessTermType = checked ? '长期' : '固定期限';
if (checked) {
this.archiveForm.businessEndDate = '';
}
this.$nextTick(() => {
this.$refs.archiveForm?.clearValidate('businessEndDate');
if (!checked) {
this.$refs.archiveForm?.validateField('businessEndDate');
}
});
},
formatBusinessLicenseValidPeriod(row = {}) {
if (row.businessTermType === '长期') {
return row.businessStartDate
? `${row.businessStartDate} 至 无固定期限`
: '无固定期限';
}
if (row.businessStartDate && row.businessEndDate) {
return `${row.businessStartDate} 至 ${row.businessEndDate}`;
}
return row.businessEndDate || row.businessStartDate || '';
},
formatFundUseRate(row = {}) {
if (row.fundUseRate === undefined || row.fundUseRate === null || row.fundUseRate === '') {
return '-';
}
const rate = Number(row.fundUseRate);
return Number.isFinite(rate) ? `${rate.toFixed(2)}%` : '-';
},
isBusinessLicenseExpiringSoon(row = {}) {
return this.isDateExpiringSoon(row.businessEndDate, row.businessTermType === '长期');
},
/** 日期是否在 30 天内到期(含当天);skip=true 时不提示(如营业执照长期) */
isDateExpiringSoon(dateValue, skip = false) {
if (skip || !dateValue) return false;
const endDate = this.$dayjs(dateValue);
if (!endDate.isValid()) return false;
const today = this.$dayjs().startOf('day');
const expireSoonDate = today.add(30, 'day');
return !endDate.isAfter(expireSoonDate, 'day');
},
handleExpireChange(expireStatus) {
this.query.expireStatus = expireStatus || 'all';
this.page.currentPage = 1;
this.onLoad(this.page, this.query);
},
handleBusinessEndDateChange(value) {
if (value) {
this.archiveForm.businessTermType = '固定期限';
}
},
handleDecimalInput(prop, value, max) {
const normalized = String(value || '')
.replace(/[^\d.]/g, '')
.replace(/^\./, '')
.replace(/(\..*)\./g, '$1')
.replace(/^(\d+)(\.\d{0,2})?.*$/, '$1$2');
if (max !== undefined && normalized !== '' && Number(normalized) > max) {
this.archiveForm[prop] = String(max);
return;
}
this.archiveForm[prop] = normalized;
},
splitValue(value) {
return value
? String(value)
.split(',')
.map(item => item.trim())
.filter(Boolean)
: [];
},
normalizeDeptIds(detail) {
// 所属组织为单选级联,只保留第一个部门 ID(历史多选数据自动收敛)
const value = detail.deptIds || detail.deptId;
const ids = value
? String(value)
.split(',')
.map(item => item.trim())
.filter(item => item !== '')
: [];
return ids.length ? [ids[0]] : [];
},
formatArchiveAddress(archive = {}) {
return [archive.registeredRegionName, archive.registeredDetailAddress]
.map(item => String(item || '').trim())
.filter(Boolean)
.join('');
},
formatContactAddress(row = {}) {
return (
row.contactAddress ||
[row.regionName, row.detailAddress]
.map(item => String(item || '').trim())
.filter(Boolean)
.join('')
);
},
// 省市区 + 详细地址为当前录入口径,registeredAddress 仅作为历史数据兜底
formatInvoiceRegisteredAddress(row = {}) {
const address = [row.registeredRegionName, row.registeredDetailAddress]
.map(item => String(item || '').trim())
.filter(Boolean)
.join('');
return address || row.registeredAddress || '';
},
formatInvoiceEmails(row = {}) {
const contacts = row.contacts || row.__raw?.contacts || [];
return (contacts || [])
.map(item => String(item.email || '').trim())
.filter(Boolean)
.join(';');
},
normalizeContact(contact = {}) {
const contactAddress =
contact.contactAddress || contact.address || this.formatContactAddress(contact);
const regionPath = Array.isArray(contact.regionPath)
? contact.regionPath
: this.findRegionPathByName(this.regionOptions, contact.regionName);
return {
...this.emptyContact(),
...contact,
regionPath,
contactAddress,
branchName:
contact.branchName ||
contact.deptName ||
this.getCurrentOrganizationName() ||
this.archiveForm.deptName ||
'',
};
},
openContact(row, index = -1) {
this.contactIndex = index;
this.contactForm = row
? this.normalizeContact(row)
: {
...this.emptyContact(),
branchName: this.getCurrentOrganizationName() || this.archiveForm.deptName || '',
};
this.contactBox = true;
this.$nextTick(() => this.syncContactRegionPath());
},
resetContact() {
this.contactIndex = -1;
this.contactForm = this.emptyContact();
this.$refs.contactForm?.clearValidate();
},
saveContact() {
this.$refs.contactForm.validate(valid => {
if (!valid) return;
const contact = this.normalizeContact({
...this.contactForm,
contactAddress: this.formatContactAddress(this.contactForm),
});
delete contact.regionPath;
if (this.contactIndex > -1) {
this.archiveForm.contacts.splice(this.contactIndex, 1, contact);
} else {
this.archiveForm.contacts.push(contact);
this.handleDetailCurrentChange('contact', this.getDetailPageCount('contact'));
}
this.contactBox = false;
});
},
deleteContact(index) {
this.$confirm('确定删除该联系人?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
this.archiveForm.contacts.splice(index, 1);
this.handleDetailCurrentChange('contact', this.contactPage.currentPage);
})
.catch(() => {});
},
normalizeReceipt(receipt = {}) {
return {
...this.emptyReceiptAccount(),
...receipt,
};
},
openReceipt(row, index = -1) {
this.receiptIndex = index;
this.receiptForm = row ? this.normalizeReceipt(row) : this.emptyReceiptAccount();
this.receiptBox = true;
},
resetReceipt() {
this.receiptIndex = -1;
this.receiptForm = this.emptyReceiptAccount();
this.$refs.receiptForm?.clearValidate();
},
// 开户行失焦后按行名精确匹配金蝶行名行号档案(A905-3),命中自动带出联行号,未命中提示检查
async fetchReceiptCnapsCode() {
const bankName = String(this.receiptForm.bankName || '').trim();
if (!bankName) return;
try {
const res = await queryKingdeeBank(bankName);
// 防止响应返回时用户已修改开户行
if (String(this.receiptForm.bankName || '').trim() !== bankName) return;
const bank = res?.data?.data || null;
if (bank && bank.unionNumber) {
this.receiptForm.cnapsCode = bank.unionNumber;
this.$refs.receiptForm?.clearValidate('cnapsCode');
} else {
this.$message.warning('未查询到金蝶开户行信息,请检查开户行名称');
}
} catch (error) {
// 忽略查询异常,联行号留空由用户手填
}
},
async saveReceipt() {
const valid = await this.$refs.receiptForm.validate().catch(() => false);
if (!valid) return;
// 保存前按联行号精确校验金蝶行名行号档案(A905-3),查不到不允许保存
const cnapsCode = String(this.receiptForm.cnapsCode || '').trim();
let bank = null;
try {
const res = await queryKingdeeBankByNumber(cnapsCode);
bank = res?.data?.data || null;
} catch (error) {
this.$message.warning('金蝶联行号校验失败,请稍后重试');
return;
}
if (!bank || !bank.unionNumber) {
this.$message.warning('未查询到金蝶联行号信息,不能保存,请检查联行号');
return;
}
const receipt = this.normalizeReceipt(this.receiptForm);
if (this.receiptIndex > -1) {
this.archiveForm.receiptAccounts.splice(this.receiptIndex, 1, receipt);
} else {
this.archiveForm.receiptAccounts.push(receipt);
this.handleDetailCurrentChange('receipt', this.getDetailPageCount('receipt'));
}
this.receiptBox = false;
},
deleteReceipt(index) {
this.$confirm('确定删除该收款信息?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
this.archiveForm.receiptAccounts.splice(index, 1);
this.handleDetailCurrentChange('receipt', this.receiptPage.currentPage);
})
.catch(() => {});
},
normalizeInvoice(invoice = {}) {
const registeredDetailAddress =
invoice.registeredDetailAddress ||
(invoice.registeredRegionName ? '' : invoice.registeredAddress) ||
'';
return {
...this.emptyInvoice(),
...invoice,
registeredRegionPath: Array.isArray(invoice.registeredRegionPath)
? invoice.registeredRegionPath
: [],
registeredDetailAddress,
registeredAddress: this.formatInvoiceRegisteredAddress({
...invoice,
registeredDetailAddress,
}),
contacts: (invoice.contacts || []).map(item => this.normalizeInvoiceContact(item)),
};
},
normalizeInvoiceContact(contact = {}) {
const deptIds = String(contact.deptIds || '')
.split(/[,,、]/)
.map(item => item.trim())
.filter(Boolean);
const deptIdList = Array.isArray(contact.deptIdList)
? contact.deptIdList.map(item => String(item)).filter(Boolean)
: deptIds;
const contactRow = {
contactName: contact.contactName || '',
contactPhone: contact.contactPhone || '',
email: contact.email || '',
deptIds: contact.deptIds || deptIdList.join(','),
deptNames: contact.deptNames || '',
deptIdList,
remark: contact.remark || '',
};
if (!this.isInternalCustomer) {
contactRow.deptIds = this.externalInvoiceDeptIds();
contactRow.deptNames = this.externalInvoiceDeptName();
contactRow.deptIdList = contactRow.deptIds
? String(contactRow.deptIds)
.split(/[,,]/)
.map(item => item.trim())
.filter(Boolean)
: [];
} else if (!contactRow.deptNames && deptIdList.length) {
contactRow.deptNames = deptIdList.map(id => this.findDeptLabel(id)).filter(Boolean).join('、');
}
return contactRow;
},
emptyInvoiceContact() {
const contact = {
contactName: '',
contactPhone: '',
email: '',
deptIds: '',
deptNames: '',
deptIdList: [],
remark: '',
};
if (!this.isInternalCustomer) {
contact.deptIds = this.externalInvoiceDeptIds();
contact.deptNames = this.externalInvoiceDeptName();
contact.deptIdList = contact.deptIds
? String(contact.deptIds)
.split(/[,,]/)
.map(item => item.trim())
.filter(Boolean)
: [];
}
return contact;
},
externalInvoiceDeptName() {
return this.archiveForm.fullName || this.archiveForm.deptName || '';
},
externalInvoiceDeptIds() {
if (this.archiveForm.deptIds) return String(this.archiveForm.deptIds);
if (Array.isArray(this.archiveForm.deptId)) return this.archiveForm.deptId.filter(Boolean).join(',');
return this.archiveForm.deptId ? String(this.archiveForm.deptId) : '';
},
defaultInvoice() {
const archive = this.archiveForm || {};
return {
...this.emptyInvoice(),
invoiceTitle: archive.fullName || '',
taxNo: archive.unifiedCreditCode || '',
registeredRegionPath: Array.isArray(archive.registeredRegionPath)
? [...archive.registeredRegionPath]
: [],
registeredRegionName: archive.registeredRegionName || '',
registeredDetailAddress: archive.registeredDetailAddress || '',
registeredAddress: this.formatArchiveAddress(archive),
contacts: [],
};
},
openInvoice(row, index = -1) {
this.invoiceIndex = index;
this.invoiceForm = row ? this.normalizeInvoice(row) : this.defaultInvoice();
this.invoiceBox = true;
this.$nextTick(() => {
this.$nextTick(() => {
this.checkOverflow('invoiceDetailInput', 'invoiceDetailOverflow');
});
});
},
resetInvoice() {
this.invoiceIndex = -1;
this.invoiceForm = this.emptyInvoice();
this.$refs.invoiceForm?.clearValidate();
},
addInvoiceContact() {
if (!Array.isArray(this.invoiceForm.contacts)) {
this.invoiceForm.contacts = [];
}
this.invoiceForm.contacts.push(this.emptyInvoiceContact());
},
removeInvoiceContact(index) {
this.invoiceForm.contacts.splice(index, 1);
},
handleInvoiceContactDeptChange(row) {
// 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('、');
},
isBlankInvoiceContact(contact = {}) {
return ![
contact.contactName,
contact.contactPhone,
contact.email,
contact.deptIds,
contact.deptNames,
...(Array.isArray(contact.deptIdList) ? contact.deptIdList : []),
contact.remark,
].some(item => String(item || '').trim());
},
validateInvoiceContacts(contacts = []) {
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
for (let index = 0; index < contacts.length; index += 1) {
const contact = contacts[index];
if (this.isBlankInvoiceContact(contact)) continue;
if (!String(contact.contactName || '').trim()) {
this.$message.warning(`请填写第${index + 1}行联系人`);
return false;
}
if (!String(contact.contactPhone || '').trim()) {
this.$message.warning(`请填写第${index + 1}行联系电话`);
return false;
}
const email = String(contact.email || '').trim();
if (email && !emailPattern.test(email)) {
this.$message.warning(`第${index + 1}行邮箱格式不正确`);
return false;
}
if (String(contact.remark || '').length > 200) {
this.$message.warning(`第${index + 1}行备注最多200个字`);
return false;
}
if (this.isInternalCustomer && !(contact.deptIdList || []).length) {
this.$message.warning(`内部组织客商请选择第${index + 1}行所属部门`);
return false;
}
}
return true;
},
saveInvoice() {
this.$refs.invoiceForm.validate(valid => {
if (!valid) return;
const invoice = this.normalizeInvoice(this.invoiceForm);
if (!this.validateInvoiceContacts(invoice.contacts)) return;
invoice.contacts = (invoice.contacts || [])
.filter(item => !this.isBlankInvoiceContact(item))
.map(item => {
const contact = { ...item };
if (this.isInternalCustomer) {
this.handleInvoiceContactDeptChange(contact);
}
delete contact.deptIdList;
return contact;
});
if (this.invoiceIndex > -1) {
this.archiveForm.invoices.splice(this.invoiceIndex, 1, invoice);
} else {
this.archiveForm.invoices.push(invoice);
this.handleDetailCurrentChange('invoice', this.getDetailPageCount('invoice'));
}
this.invoiceBox = false;
});
},
deleteInvoice(index) {
this.$confirm('确定删除该发票信息?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
this.archiveForm.invoices.splice(index, 1);
this.handleDetailCurrentChange('invoice', this.invoicePage.currentPage);
})
.catch(() => {});
},
deleteScoreAttachment(index) {
this.$confirm('确定删除该评分证明材料?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
this.scoreAttachmentFiles.splice(index, 1);
})
.catch(() => {});
},
parseAttachments(value) {
if (!value) return [];
if (Array.isArray(value)) return value;
try {
const list = JSON.parse(value);
return Array.isArray(list)
? list.map(item => ({
...item,
files: item.url ? [item] : [],
}))
: [];
} catch (error) {
return String(value)
.split(',')
.map(item => ({ name: item.trim(), url: item.trim() }))
.filter(item => item.url)
.map(item => ({
...item,
files: [item],
}));
}
},
stringifyAttachments(list) {
const files = (list || [])
.filter(item => item.url)
.map(item => ({
type: String(item.type || '').trim(),
originalName: String(item.originalName || item.name || '').trim(),
name: String(item.originalName || item.name || '').trim(),
description: String(item.description || '').trim(),
size: String(item.size || '').trim(),
uploadUserName: String(item.uploadUserName || '').trim(),
uploadTime: String(item.uploadTime || '').trim(),
url: String(item.url || '').trim(),
}))
.filter(item => item.url);
return files.length ? JSON.stringify(files) : '';
},
addAttachment() {
this.qualificationFiles.push({
type: '',
name: '',
description: '',
size: '',
uploadUserName: this.userInfo.realName || this.userInfo.userName || '',
uploadTime: this.$dayjs().format('YYYY-MM-DD HH:mm:ss'),
url: '',
files: [],
});
},
formatAttachmentSize(size) {
if (!size) return '';
const byteSize = Number(size);
if (!Number.isFinite(byteSize)) return String(size);
const mb = byteSize / 1024 / 1024;
if (mb >= 1) return `${mb.toFixed(2)} MB`;
return `${(byteSize / 1024).toFixed(2)} KB`;
},
handleQualificationUploadSuccess(file) {
this.addQualificationFile(file);
},
handleOcrQualificationUploadSuccess(file) {
this.addQualificationFile(file);
const originalName = file.originalName || file.name || '客商材料';
if (!originalName.includes('营业执照') || !this.isQualificationImage(file)) return;
this.recognizeBusinessLicense(file.url || file.link || '');
},
addQualificationFile(file) {
const originalName = file.originalName || file.name || '客商材料';
this.qualificationFiles.push({
type: this.resolveQualificationType(originalName),
originalName,
name: originalName,
description: '',
size: this.formatAttachmentSize(file.size),
uploadUserName: this.userInfo.realName || this.userInfo.userName || '',
uploadTime: this.$dayjs().format('YYYY-MM-DD HH:mm:ss'),
url: file.url || file.link || '',
files: [file],
});
this.sortQualificationFiles();
},
recognizeBusinessLicense(url) {
if (!url) {
this.$message.warning('营业执照上传成功,未获取到图片地址,无法自动识别');
return;
}
const loading = ElLoading.service({
lock: true,
text: '营业执照识别中',
background: 'rgba(255, 255, 255, 0.7)',
});
recognizeBusinessLicenseOcr(url)
.then(res => {
this.applyBusinessLicenseRecognition(res?.data?.data || {});
this.$message.success('营业执照识别完成');
})
.catch(() => {
this.$message.warning('营业执照上传成功,自动识别失败,请手动填写工商信息');
})
.finally(() => loading.close());
},
normalizeBusinessLicenseOcrFields(result = {}) {
if (Array.isArray(result)) {
return Object.fromEntries(
result
.filter(item => item && item.key)
.map(item => [String(item.key).trim(), String(item.value ?? '').trim()])
.filter(([, value]) => value)
);
}
if (result.fields && typeof result.fields === 'object' && !Array.isArray(result.fields)) {
return result.fields;
}
if (Array.isArray(result.data)) {
return this.normalizeBusinessLicenseOcrFields(result.data);
}
return result;
},
getBusinessLicenseOcrValue(result, labels) {
const fields = this.normalizeBusinessLicenseOcrFields(result);
for (const label of labels) {
const value = fields[label];
const text = value && typeof value === 'object' ? value.words : value;
if (text !== undefined && text !== null && String(text).trim()) return String(text).trim();
}
const words = result?.words_result || result?.wordsResult || {};
for (const label of labels) {
const value = words[label];
const text = value && typeof value === 'object' ? value.words : value;
if (text !== undefined && text !== null && String(text).trim()) return String(text).trim();
}
return '';
},
isNoFixedBusinessTerm(value) {
return String(value || '').replace(/\s/g, '') === '无';
},
applyBusinessLicenseRecognition(result = {}) {
const fields = this.normalizeBusinessLicenseOcrFields(result);
const code = this.getBusinessLicenseOcrValue(fields, [
'uscc',
'统一社会信用代码',
'社会信用代码',
'注册号',
]);
const name = this.getBusinessLicenseOcrValue(fields, [
'company_name',
'单位名称',
'企业名称',
'名称',
]);
const legalPerson = this.getBusinessLicenseOcrValue(fields, [
'legal_name',
'法定代表人',
'法人',
]);
const address = this.getBusinessLicenseOcrValue(fields, ['add', '地址', '住所']);
const capital = this.getBusinessLicenseOcrValue(fields, ['capital', '注册资本']);
const paidInCapital = this.getBusinessLicenseOcrValue(fields, ['实缴资本', '实收资本']);
const term = this.getBusinessLicenseOcrValue(fields, ['period', '营业期限', '有效期']);
const termStartDate = this.getBusinessLicenseOcrValue(fields, [
'establishment_date',
'有效期起始日期',
'成立日期',
'核准日期',
]);
const scope = this.getBusinessLicenseOcrValue(fields, ['经营范围']);
if (code) this.archiveForm.unifiedCreditCode = code.replace(/\s/g, '').toUpperCase();
if (name) this.archiveForm.fullName = name;
if (name) this.checkArchiveNameRisk();
if (legalPerson) this.archiveForm.legalPerson = legalPerson;
if (address) {
this.archiveForm.registeredAddress = address;
if (!this.archiveForm.registeredDetailAddress)
this.archiveForm.registeredDetailAddress = address;
}
if (capital) {
const normalizedCapital = capital.replace(/,/g, '').match(/\d+(?:\.\d+)?/);
if (normalizedCapital) this.archiveForm.registeredCapital = normalizedCapital[0];
}
if (paidInCapital) {
const normalizedPaidIn = paidInCapital.replace(/,/g, '').match(/\d+(?:\.\d+)?/);
if (normalizedPaidIn) this.archiveForm.paidInCapital = normalizedPaidIn[0];
}
this.applyBusinessTermRecognition(term, termStartDate);
if (this.isNoFixedBusinessTerm(term) || this.isNoFixedBusinessTerm(termStartDate)) {
this.handleNoFixedTermChange(true);
}
if (scope) {
this.archiveForm.businessScope = scope;
}
this.$nextTick(() => {
[
'unifiedCreditCode',
'fullName',
'legalPerson',
'registeredCapital',
'paidInCapital',
'businessStartDate',
'businessEndDate',
'businessScope',
'registeredDetailAddress',
].forEach(prop => this.$refs.archiveForm?.validateField(prop));
});
},
normalizeOcrDate(value) {
if (!value) return '';
const match = String(value).match(/(\d{4})[年./-](\d{1,2})[月./-](\d{1,2})/);
if (!match) return '';
const month = String(match[2]).padStart(2, '0');
const day = String(match[3]).padStart(2, '0');
return `${match[1]}-${month}-${day}`;
},
applyBusinessTermRecognition(term, termStartDate = '') {
const startFromField = this.normalizeOcrDate(termStartDate);
if (startFromField) {
this.archiveForm.businessStartDate = startFromField;
}
if (!term) return;
if (/长期|永久|无固定/.test(term) || this.isNoFixedBusinessTerm(term)) {
this.archiveForm.businessTermType = '长期';
this.archiveForm.businessEndDate = '';
return;
}
const dateMatches = String(term).match(/\d{4}[年./-]\d{1,2}[月./-]\d{1,2}/g) || [];
const startDate = this.normalizeOcrDate(dateMatches[0]) || startFromField;
const endDate = this.normalizeOcrDate(dateMatches[1]);
if (startDate) {
this.archiveForm.businessStartDate = startDate;
}
if (endDate) {
this.archiveForm.businessTermType = '固定期限';
this.archiveForm.businessEndDate = endDate;
return;
}
if (startDate && dateMatches.length === 1 && !/至|到|-|—|~/.test(term)) {
// 仅识别到一个日期且无区间分隔时,按截止日处理(兼容旧 OCR 结果)
this.archiveForm.businessTermType = '固定期限';
this.archiveForm.businessEndDate = startDate;
}
},
getQualificationFileUrl(file = {}) {
return file.url || file.link || file.domain || '';
},
getQualificationFileExtension(file = {}) {
const name = String(
file.originalName || file.name || this.getQualificationFileUrl(file) || ''
).split('?')[0];
const index = name.lastIndexOf('.');
return index > -1 ? name.slice(index + 1).toLowerCase() : '';
},
isQualificationImage(file) {
return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(
this.getQualificationFileExtension(file)
);
},
isQualificationPdf(file) {
const mimeType = String(file.mimeType || file.contentType || '').toLowerCase();
return (
mimeType.includes('application/pdf') || this.getQualificationFileExtension(file) === 'pdf'
);
},
previewQualificationFile(file) {
const url = this.getQualificationFileUrl(file);
if (!url) {
this.$message.warning('附件地址为空,无法预览');
return;
}
if (this.isQualificationImage(file)) {
this.qualificationImagePreviewUrls = this.qualificationFiles
.filter(item => this.isQualificationImage(item) && this.getQualificationFileUrl(item))
.map(item => this.getQualificationFileUrl(item));
this.qualificationImagePreviewIndex = Math.max(
this.qualificationImagePreviewUrls.indexOf(url),
0
);
this.qualificationImagePreviewVisible = true;
return;
}
const isPdf = this.isQualificationPdf(file);
this.qualificationPreviewFile = {
name: file.originalName || file.name || '附件',
url,
mimeType: isPdf ? 'application/pdf' : file.mimeType || file.contentType || '',
isPdf,
};
this.qualificationDocumentPreviewVisible = true;
},
handleQualificationPreviewUnsupported() {
this.$message.warning('当前文件暂不支持在线预览');
},
handleQualificationPreviewError() {
this.$message.error('附件预览失败');
},
resolveQualificationType(fileName) {
const name = String(fileName || '').toLocaleLowerCase();
const matchedType = [...this.qualificationTypeOptions]
.filter(item => item?.label || item?.value)
.sort(
(a, b) =>
String(b.label || b.value || '').length - String(a.label || a.value || '').length
)
.find(item => {
const typeText = String(item.label || item.value || '').toLocaleLowerCase();
return typeText && name.includes(typeText);
});
if (matchedType?.value) return matchedType.value;
const otherType = this.qualificationTypeOptions.find(item =>
String(item.label || item.value || '').includes('其他')
);
if (otherType?.value) return otherType.value;
this.qualificationTypeOptions.push({ label: '其他附件', value: '其他附件' });
return '其他附件';
},
getQualificationTypeOrder(type) {
const normalizedType = String(type || '').trim();
const index = this.qualificationTypeOptions.findIndex(
item =>
String(item.value || '').trim() === normalizedType ||
String(item.label || '').trim() === normalizedType
);
return index === -1 ? Number.MAX_SAFE_INTEGER : index;
},
sortQualificationFiles() {
this.qualificationFiles = (this.qualificationFiles || [])
.map((file, index) => ({ file, index }))
.sort((a, b) => {
const orderDifference =
this.getQualificationTypeOrder(a.file.type) -
this.getQualificationTypeOrder(b.file.type);
return orderDifference || a.index - b.index;
})
.map(item => item.file);
},
getRequiredQualificationTypes(customerType = this.archiveForm.customerType) {
const customerTypes = Array.isArray(customerType)
? customerType
: this.splitValue(customerType);
const requiredTypes = ['营业执照', '法人身份证'];
const includesCarrier = customerTypes.some(item =>
['承运商', 'carrier'].includes(
String(item || '')
.trim()
.toLowerCase()
)
);
if (includesCarrier) requiredTypes.push('运输许可证');
return requiredTypes;
},
isUploadedQualificationType(uploadedType, requiredType) {
const uploaded = String(uploadedType || '')
.replace(/\s/g, '')
.trim();
if (uploaded === requiredType || uploaded.includes(requiredType)) return true;
return requiredType === '运输许可证' && /运输.*许可证/.test(uploaded);
},
getMissingQualificationTypes(
files = this.qualificationFiles,
customerType = this.archiveForm.customerType
) {
const uploadedTypes = new Set(
(files || [])
.filter(item => item.url)
.map(item => String(item.type || '').trim())
.filter(Boolean)
);
return this.getRequiredQualificationTypes(customerType).filter(
requiredType =>
![...uploadedTypes].some(uploadedType =>
this.isUploadedQualificationType(uploadedType, requiredType)
)
);
},
downloadAttachments() {
const source = this.selectedQualificationFiles.length
? this.selectedQualificationFiles
: this.qualificationFiles;
const files = source.filter(item => item.url);
if (!files.length) {
this.$message.warning(
this.selectedQualificationFiles.length ? '所选附件暂无可下载地址' : '暂无可下载附件'
);
return;
}
files.forEach((item, index) => {
window.setTimeout(() => {
downloadFileByUrl(item.url, item.originalName || item.name || `客商材料${index + 1}`);
}, index * 300);
});
},
handleQualificationSelectionChange(rows) {
this.selectedQualificationFiles = rows || [];
},
replaceNegativeOneWithBlank(value) {
if (value === -1 || value === '-1') return '';
if (Array.isArray(value)) {
return value.map(item => this.replaceNegativeOneWithBlank(item));
}
if (value && typeof value === 'object') {
return Object.keys(value).reduce((result, key) => {
result[key] = this.replaceNegativeOneWithBlank(value[key]);
return result;
}, {});
}
return value;
},
normalizeDetail(detail) {
const registeredDetailAddress =
detail.registeredDetailAddress ||
(detail.registeredRegionName ? '' : detail.registeredAddress) ||
'';
const archive = {
...this.emptyArchive(),
...detail,
deptId: this.normalizeDeptIds(detail),
customerType: this.splitValue(detail.customerType),
customerKind: detail.customerKind || 'external',
invoiceTaxRate: this.normalizeOptionalAmount(detail.invoiceTaxRate),
registeredCapital: this.normalizeOptionalAmount(detail.registeredCapital),
paidInCapital: this.normalizeOptionalAmount(detail.paidInCapital),
maxCreditLimit: this.normalizeOptionalAmount(detail.maxCreditLimit),
applyCreditLimit: this.normalizeOptionalAmount(detail.applyCreditLimit),
usedCreditLimit: this.normalizeOptionalAmount(detail.usedCreditLimit),
businessScope: detail.businessScope || '',
businessTermType: detail.businessTermType || '固定期限',
businessStartDate: detail.businessStartDate || '',
businessEndDate: detail.businessEndDate || '',
networkFreightPlatform:
detail.networkFreightPlatform === 0 || detail.networkFreightPlatform === 1
? detail.networkFreightPlatform
: null,
guangxiTop100:
detail.guangxiTop100 === 0 || detail.guangxiTop100 === 1 ? detail.guangxiTop100 : null,
registeredRegionPath: Array.isArray(detail.registeredRegionPath)
? detail.registeredRegionPath
: [],
registeredRegionName: detail.registeredRegionName || '',
registeredDetailAddress,
contacts: (detail.contacts || []).map(item =>
this.normalizeContact({
...item,
branchName:
item.branchName ||
item.deptName ||
this.getCurrentOrganizationName() ||
detail.deptName ||
'',
})
),
receiptAccounts: (detail.receiptAccounts || []).map(item => this.normalizeReceipt(item)),
invoices: (detail.invoices || []).map(item => this.normalizeInvoice(item)),
scores: (detail.scores || []).map(score => this.calculateScore(this.normalizeScore(score))),
changeRecords: detail.changeRecords || [],
};
return this.applyArchiveCreditFromScores(archive);
},
normalizeScore(score) {
return {
quantificationId: '',
evaluateMode: 'online',
scoreDate: this.$dayjs().format('YYYY-MM-DD'),
nextEvaluateDate: '',
applyCreditLimit: this.archiveForm.applyCreditLimit || '',
maxCreditLimit: '',
maxMinusScore: HARDCODED_MAX_MINUS_SCORE,
maxPlusScore: HARDCODED_MAX_PLUS_SCORE,
creditLevel: '',
standards: [],
tempApplyCreditLimit: '',
tempCreditStartDate: '',
tempCreditEndDate: '',
remark: '',
selfStatus: '未完成',
reviewStatus: '未完成',
proofAttachments: '',
attachments: [],
details: [],
...score,
quantificationId: score.quantificationId ? String(score.quantificationId) : '',
evaluateMode: score.evaluateMode === 'offline' ? 'offline' : 'online',
nextEvaluateDate: score.nextEvaluateDate || '',
selfScore: this.normalizeScoreInputValue(score.selfScore),
reviewScore: this.normalizeScoreInputValue(score.reviewScore),
finalScore: this.normalizeScoreInputValue(score.finalScore),
applyCreditLimit: this.normalizeOptionalAmount(
score.applyCreditLimit === undefined
? this.archiveForm.applyCreditLimit
: score.applyCreditLimit
),
maxCreditLimit: this.normalizeOptionalAmount(score.maxCreditLimit),
maxMinusScore: HARDCODED_MAX_MINUS_SCORE,
maxPlusScore: HARDCODED_MAX_PLUS_SCORE,
tempApplyCreditLimit: this.normalizeOptionalAmount(score.tempApplyCreditLimit),
attachments: score.attachments || this.parseAttachments(score.proofAttachments),
details: (score.details || []).map(detail => ({
...detail,
scoreInput: this.normalizeScoreInputValue(detail.scoreInput),
selfScore: this.normalizeScoreInputValue(detail.selfScore),
reviewScore: this.normalizeScoreInputValue(detail.reviewScore),
})),
};
},
normalizeOptionalAmount(value) {
const amount = Number(value);
return Number.isFinite(amount) && amount < 0 ? '' : value ?? '';
},
getScoreTimeValue(value) {
if (!value) return 0;
const time = this.$dayjs(value).valueOf();
return Number.isFinite(time) ? time : 0;
},
sortScoresByLatest(scores = []) {
return (scores || [])
.map((score, index) => ({ score, index }))
.sort((a, b) => {
const scoreDateDiff =
this.getScoreTimeValue(b.score.scoreDate) - this.getScoreTimeValue(a.score.scoreDate);
if (scoreDateDiff) return scoreDateDiff;
const updateTimeDiff =
this.getScoreTimeValue(b.score.updateTime) - this.getScoreTimeValue(a.score.updateTime);
if (updateTimeDiff) return updateTimeDiff;
const createTimeDiff =
this.getScoreTimeValue(b.score.createTime) - this.getScoreTimeValue(a.score.createTime);
if (createTimeDiff) return createTimeDiff;
return b.index - a.index;
});
},
getLatestCreditScore(scores = []) {
return this.sortScoresByLatest(scores).filter(item =>
String(item.score.creditLevel || '').trim()
)[0]?.score;
},
/** 曾审核通过后:删除/重建评分在再次审核通过前,主表等级与额度保持旧值暂时生效 */
shouldFreezeEffectiveCredit(archive = this.archiveForm) {
return Boolean(archive?.approvedTime);
},
applyArchiveCreditFromScores(archive) {
if (this.shouldFreezeEffectiveCredit(archive)) {
return archive;
}
const latestScore = this.getLatestCreditScore(archive.scores || []);
archive.customerLevel = latestScore?.creditLevel || archive.customerLevel || '';
archive.maxCreditLimit = latestScore?.maxCreditLimit || archive.maxCreditLimit || '';
archive.applyCreditLimit = archive.maxCreditLimit;
return archive;
},
syncArchiveCreditFromScores() {
this.applyArchiveCreditFromScores(this.archiveForm);
},
openArchivePage(row) {
const isEdit = Boolean(row?.id);
this.$router.push({
path: customerArchiveFormRoute,
query: isEdit ? { id: row.id, name: '编辑客商档案' } : { name: '新增客商档案' },
});
},
viewArchive(row) {
this.$router.push({
path: customerArchiveFormRoute,
query: { id: row.id, name: '查看客商档案', view: '1' },
});
},
openReview(row) {
this.$router.push({
path: customerArchiveFormRoute,
query: { id: row.id, name: '客商复评', review: '1' },
});
},
closeArchive() {
if (this.isPublicViewPage) return;
this.archiveBox = false;
this.markCustomerArchiveListNeedRefresh();
this.$router.$avueRouter?.closeTag?.();
this.$router.push(customerArchiveListRoute);
},
openArchive(row, readonly = false) {
this.readonly = readonly;
this.activeTab = 'scores';
this.resetDetailPagination();
this.resetChangeRecordPage();
if (row && row.id) {
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 || '';
const qualificationFiles = this.parseAttachments(
this.archiveForm.qualificationAttachments
);
this.qualificationFiles = readonly
? this.replaceNegativeOneWithBlank(qualificationFiles)
: qualificationFiles;
this.sortQualificationFiles();
this.qualificationUploadFiles = [];
this.ocrQualificationUploadFiles = [];
this.selectedQualificationFiles = [];
if (this.isPublicViewPage) this.ensurePublicViewOptions(this.archiveForm);
this.applyFixedOwnerDept();
this.checkArchiveNameRisk();
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();
this.originalAccessType = '';
this.resetKingdeeQueryState();
this.qualificationFiles = [];
this.qualificationUploadFiles = [];
this.ocrQualificationUploadFiles = [];
this.selectedQualificationFiles = [];
this.applyFixedOwnerDept();
this.checkArchiveNameRisk();
this.archiveBox = true;
},
resetArchive() {
this.readonly = false;
this.resetKingdeeQueryState();
this.changeRecordDetailVisible = false;
this.changeRecordDetail = null;
this.changeRecordDetailRows = [];
this.archiveForm = this.emptyArchive();
this.originalAccessType = '';
this.qualificationFiles = [];
this.qualificationUploadFiles = [];
this.ocrQualificationUploadFiles = [];
this.selectedQualificationFiles = [];
this.scoreBox = false;
this.resetScoreDialog();
this.resetDetailPagination();
this.resetChangeRecordPage();
this.contactBox = false;
this.contactIndex = -1;
this.contactForm = this.emptyContact();
this.receiptBox = false;
this.receiptIndex = -1;
this.receiptForm = this.emptyReceiptAccount();
this.invoiceBox = false;
this.invoiceIndex = -1;
this.invoiceForm = this.emptyInvoice();
this.$refs.archiveForm?.clearValidate();
},
// 仅客商分类含「客户」时强制评分;纯承运商不强制。评分记录至多一条。
validateScoreForApproval(archive) {
const scores = archive.scores || [];
if (scores.length > 1) {
this.$message.warning('评分记录只能有一条');
return false;
}
if (!this.requiresCustomerScore(archive.customerType)) {
return true;
}
if (scores.length !== 1 || scores[0].selfStatus !== '已完成') {
this.$message.warning('客户提交审批前必须完成评分自评');
return false;
}
if (!scores[0].nextEvaluateDate) {
this.$message.warning('请填写下次评定日期');
return false;
}
return true;
},
/** 客商分类是否包含客户(字典值「客户」或历史值 customer) */
requiresCustomerScore(customerType) {
const values = Array.isArray(customerType)
? customerType
: String(customerType || '')
.replace(/,/g, ',')
.split(',');
return values
.map(item => String(item || '').trim())
.filter(Boolean)
.some(item => item === '客户' || item.toLowerCase() === 'customer');
},
normalizeArchive() {
// 提交前再次固定所属组织,防止被绕过
this.applyFixedOwnerDept();
const archive = JSON.parse(JSON.stringify(this.archiveForm));
if (archive.businessTermType === '长期') {
archive.businessEndDate = null;
}
if (!archive.businessStartDate) {
archive.businessStartDate = null;
}
const invoiceTaxRate = this.normalizeOptionalAmount(archive.invoiceTaxRate);
const registeredCapital = this.normalizeOptionalAmount(archive.registeredCapital);
const paidInCapital = this.normalizeOptionalAmount(archive.paidInCapital);
const usedCreditLimit = this.normalizeOptionalAmount(archive.usedCreditLimit);
archive.invoiceTaxRate = invoiceTaxRate === '' ? null : invoiceTaxRate;
archive.registeredCapital = registeredCapital === '' ? null : registeredCapital;
archive.paidInCapital = paidInCapital === '' ? null : paidInCapital;
archive.usedCreditLimit = usedCreditLimit === '' ? null : usedCreditLimit;
archive.customerType = Array.isArray(archive.customerType)
? archive.customerType.join(',')
: archive.customerType;
archive.businessScope = String(archive.businessScope || '').trim();
archive.networkFreightPlatform =
archive.networkFreightPlatform === 0 || archive.networkFreightPlatform === 1
? archive.networkFreightPlatform
: null;
archive.guangxiTop100 =
archive.guangxiTop100 === 0 || archive.guangxiTop100 === 1 ? archive.guangxiTop100 : null;
const deptIds = Array.isArray(archive.deptId)
? archive.deptId
: archive.deptId
? [archive.deptId]
: [];
const deptNames = deptIds
.map(id => this.deptOptions.find(item => String(item.value) === String(id))?.rawLabel)
.filter(Boolean);
archive.deptIds = deptIds.join(',');
archive.deptId = deptIds.length ? Number(deptIds[0]) : null;
archive.deptName = deptNames.length ? deptNames.join(',') : archive.deptName;
archive.registeredAddress = this.formatArchiveAddress(archive);
delete archive.registeredRegionPath;
archive.qualificationAttachments = this.stringifyAttachments(this.qualificationFiles);
archive.contacts = (archive.contacts || [])
.map(item => {
const contact = this.normalizeContact({
...item,
contactAddress: this.formatContactAddress(item),
});
delete contact.regionPath;
return contact;
})
.filter(item => item.contactName || item.contactPhone);
// 主表联系电话改由联系人维护,取第一条有效手机号回填
const firstContactPhone = (archive.contacts || [])
.map(item => String(item.contactPhone || '').trim())
.find(Boolean);
archive.contactPhone = firstContactPhone || null;
archive.receiptAccounts = (archive.receiptAccounts || [])
.map(item => this.normalizeReceipt(item))
.filter(item => item.accountName || item.accountHolderName || item.bankAccount || item.cnapsCode);
archive.invoices = (archive.invoices || [])
.map(item => {
const invoice = this.normalizeInvoice(item);
delete invoice.registeredRegionPath;
invoice.contacts = (invoice.contacts || [])
.filter(contact => !this.isBlankInvoiceContact(contact))
.map(contact => {
const row = { ...contact };
if (this.isInternalCustomer) {
this.handleInvoiceContactDeptChange(row);
}
delete row.deptIdList;
return row;
});
return invoice;
})
.filter(item => item.invoiceTitle || item.taxNo || item.bankAccount);
archive.scores = (archive.scores || []).map(score => {
const calculatedScore = this.calculateScore(score);
const scoreMaxCreditLimit = this.normalizeOptionalAmount(calculatedScore.maxCreditLimit);
const scoreApplyCreditLimit = this.normalizeOptionalAmount(
calculatedScore.applyCreditLimit
);
calculatedScore.maxCreditLimit = scoreMaxCreditLimit === '' ? null : scoreMaxCreditLimit;
calculatedScore.applyCreditLimit =
scoreApplyCreditLimit === '' ? null : scoreApplyCreditLimit;
return calculatedScore;
});
this.applyArchiveCreditFromScores(archive);
const maxCreditLimit = this.normalizeOptionalAmount(archive.maxCreditLimit);
const applyCreditLimit = this.normalizeOptionalAmount(archive.applyCreditLimit);
archive.maxCreditLimit = maxCreditLimit === '' ? null : maxCreditLimit;
archive.applyCreditLimit = applyCreditLimit === '' ? null : applyCreditLimit;
return archive;
},
validateContactsRequired(archive = this.archiveForm) {
const contacts = (archive.contacts || []).filter(
item => String(item.contactName || '').trim() || String(item.contactPhone || '').trim()
);
if (!contacts.length) {
this.$message.warning('请至少维护一条联系人');
this.activeTab = 'contacts';
return false;
}
return true;
},
saveArchive() {
if (this.kingdeeBlocked) {
this.$message.warning('金蝶审核中,请稍后重新添加');
return;
}
this.applyFixedOwnerDept();
this.$refs.archiveForm.validate(valid => {
if (!valid) {
this.$message.warning('请先完整填写必填项');
return;
}
if (!this.validateContactsRequired()) return;
submit(this.normalizeArchive()).then(() => {
this.closeArchive();
this.onLoad(this.page);
this.$message({ type: 'success', message: '操作成功!' });
});
});
},
saveAndSubmitArchive() {
if (this.kingdeeBlocked) {
this.$message.warning('金蝶审核中,请稍后重新添加');
return;
}
this.applyFixedOwnerDept();
this.$refs.archiveForm.validate(valid => {
if (!valid) {
this.$message.warning('请先完整填写必填项');
return;
}
if (!this.validateContactsRequired()) return;
const archive = this.normalizeArchive();
// 仅提交时落变更记录;保存不记录
archive.recordChange = true;
if (!this.validateScoreForApproval(archive)) return;
const loading = ElLoading.service({
lock: true,
text: '提交中',
background: 'rgba(255, 255, 255, 0.7)',
});
submit(archive)
.then(res => {
const data = res.data.data;
const id = (data && typeof data === 'object' ? data.id : data) || this.archiveForm.id;
if (!id) {
this.closeArchive();
this.onLoad(this.page);
this.$message({ type: 'success', message: '保存成功,请在列表提交审批' });
return;
}
const fullName =
(data && typeof data === 'object' ? data.fullName : '') ||
archive.fullName ||
this.archiveForm.fullName ||
'';
return this.submitCustomerApproval(
id,
fullName,
archive.approvalStatus || this.archiveForm.approvalStatus
).then(() => {
this.closeArchive();
this.onLoad(this.page);
this.$message({ type: 'success', message: '提交成功!' });
});
})
.finally(() => loading.close());
});
},
/**
* 列表 / 新增 / 编辑提交共用:
* 未开启 MK 时仅走本地提交审批(与新增一致,不请求 MK 流程);
* 开启 MK 时先提交审核流,再更新客商审批状态。
*/
submitCustomerApproval(id, fullName = '', approvalStatus = '') {
const submitLocal = () => submitApproval(id);
const run = enabled => {
this.mkEnabled = !!enabled;
this._mkEnabledResolved = true;
if (!this.mkEnabled) {
return submitLocal();
}
return submitMkApprovalFlow({
bizType: 'customer-archive',
bizId: id,
subjectName: fullName,
approvalStatus,
mkEnabled: true,
}).then(submitLocal);
};
// 与列表按钮一致:以页面 mkEnabled 为准,未开启时不请求 MK
if (this._mkEnabledResolved) {
return run(this.mkEnabled);
}
return loadMkEnabled()
.then(enabled => run(enabled))
.catch(() => run(false));
},
openReviewScore() {
const scores = this.archiveForm.scores || [];
if (!scores.length) {
this.$message.warning('暂无评分记录,无法复评');
return;
}
this.$nextTick(() => this.openScore(scores[0], 0));
},
/** 提交后详情页评分表:审批中且未完成复评时可复评 */
canReviewScore(row = {}) {
return (
this.hasPermission('customer_type_re_cert') &&
this.archiveForm.approvalStatus === 'reviewing' &&
row.reviewStatus !== '已完成'
);
},
startReviewScore(row, index = 0) {
if (!row) {
this.$message.warning('暂无评分记录,无法复评');
return;
}
// 详情只读进入时也要能改复评得分并确认
this.reviewMode = true;
this.openScore(row, index);
},
addScore() {
const existingScores = this.archiveForm.scores || [];
if (existingScores.length) {
this.$message.warning(
this.shouldFreezeEffectiveCredit()
? '请先删除原评分记录后再添加新评分表'
: '请勿重复添加'
);
return;
}
this.scoreRecordIndex = -1;
this.currentScore = this.normalizeScore({ applyCreditLimit: '' });
this.scoreAttachmentFiles = [];
this.expandedScoreCategoryKeys = [];
this.scoreBox = true;
if (!this.scoreQuantificationOptions.length) {
this.initScoreQuantificationOptions();
}
},
openScore(row, index = -1) {
this.scoreRecordIndex = index;
this.currentScore = this.normalizeScore(JSON.parse(JSON.stringify(row || {})));
this.scoreAttachmentFiles = JSON.parse(JSON.stringify(this.currentScore.attachments || []));
this.expandedScoreCategoryKeys = [];
this.scoreBox = true;
const loadOptions = this.scoreQuantificationOptions.length
? Promise.resolve()
: this.initScoreQuantificationOptions();
loadOptions
.then(() => this.ensureScoreQuantificationOption(this.currentScore.quantificationId))
.then(() => this.applyMaxMinusScoreFromQuantification(this.currentScore.quantificationId));
},
resetScoreDialog() {
this.scoreRecordIndex = -1;
this.currentScore = { details: [] };
this.scoreAttachmentFiles = [];
this.expandedScoreCategoryKeys = [];
},
handleScoreQuantificationChange(quantificationId) {
if (!quantificationId) {
this.currentScore.details = [];
this.expandedScoreCategoryKeys = [];
return;
}
Promise.allSettled([
getScoreTemplate(quantificationId),
getCreditScoreQuantificationDetail(quantificationId),
]).then(([templateResult, quantificationResult]) => {
const templateData =
templateResult.status === 'fulfilled' ? templateResult.value.data.data || {} : {};
const quantificationData =
quantificationResult.status === 'fulfilled'
? quantificationResult.value.data.data || {}
: {};
const scoreTemplate =
templateData.categories?.length || templateData.details?.length
? templateData
: quantificationData;
const template = this.normalizeScore({
...scoreTemplate,
quantificationId,
evaluateMode: this.currentScore.evaluateMode || 'online',
scoreDate: this.currentScore.scoreDate,
nextEvaluateDate: this.currentScore.nextEvaluateDate,
applyCreditLimit: this.currentScore.applyCreditLimit,
maxCreditLimit: '',
creditLevel: '',
maxMinusScore: HARDCODED_MAX_MINUS_SCORE,
maxPlusScore: HARDCODED_MAX_PLUS_SCORE,
tempApplyCreditLimit: this.currentScore.tempApplyCreditLimit,
tempCreditStartDate: this.currentScore.tempCreditStartDate,
tempCreditEndDate: this.currentScore.tempCreditEndDate,
remark: this.currentScore.remark,
attachments: this.scoreAttachmentFiles,
standards: quantificationData.standards || scoreTemplate.standards || [],
details: this.normalizeScoreTemplateDetails(scoreTemplate),
});
if (!template.details || !template.details.length) {
this.$message.warning('当前评分量化表未配置评分项目');
}
this.currentScore = template;
this.expandedScoreCategoryKeys = [];
this.calculateScore(this.currentScore);
});
},
applyMaxMinusScoreFromQuantification(quantificationId) {
if (!quantificationId) return Promise.resolve();
// 暂定写死加分/减分上限,不再依赖量化表配置值
this.currentScore.maxMinusScore = HARDCODED_MAX_MINUS_SCORE;
this.currentScore.maxPlusScore = HARDCODED_MAX_PLUS_SCORE;
this.calculateScore(this.currentScore);
return Promise.resolve();
},
normalizeScoreTemplateDetails(template = {}) {
const categoryDetails = (template.categories || []).flatMap(category =>
(category.items || []).map(item => ({
...item,
categoryCode: category.categoryCode,
categoryName: category.categoryName,
scoreInput: this.normalizeScoreInputValue(item.scoreInput),
optionsJson:
item.optionsJson ||
JSON.stringify(
(item.options || []).map(option => ({
label: option.label || option.optionName || option.value,
value: option.value ?? option.optionName ?? option.label,
score: option.score,
changeType: option.changeType,
changeValue: option.changeValue,
changeUnit: option.changeUnit,
scoreType: option.scoreType,
}))
),
}))
);
if (!template.details || !template.details.length) return categoryDetails;
return template.details.map(detail => {
const category = (template.categories || []).find(item => {
if (detail.categoryCode) return item.categoryCode === detail.categoryCode;
if (detail.categoryName) return item.categoryName === detail.categoryName;
return (item.items || []).some(scoreItem => {
if (detail.itemId && scoreItem.id)
return String(scoreItem.id) === String(detail.itemId);
return scoreItem.itemName && scoreItem.itemName === detail.itemName;
});
});
return {
...detail,
categoryCode: detail.categoryCode || category?.categoryCode || '',
categoryName: detail.categoryName || category?.categoryName || '',
scoreInput: this.normalizeScoreInputValue(detail.scoreInput),
};
});
},
normalizeScoreInputValue(value) {
if (value === undefined || value === null || value === '' || value === -1 || value === '-1') {
return '';
}
return String(value);
},
getScoreDetailCategoryCode(detail = {}) {
const rawCode = String(
detail.categoryCode ||
detail.scoreCategory ||
detail.categoryType ||
detail.itemCategory ||
detail.type ||
''
).toLowerCase();
if (['basic', 'plus', 'minus'].includes(rawCode)) return rawCode;
if (rawCode === '1') return 'basic';
if (rawCode === '2') return 'plus';
if (rawCode === '3') return 'minus';
const categoryText = [
detail.categoryName,
detail.scoreCategoryName,
detail.category,
detail.categoryTypeName,
detail.typeName,
]
.filter(Boolean)
.join(' ');
if (categoryText.includes('减分')) return 'minus';
if (categoryText.includes('加分')) return 'plus';
if (categoryText.includes('基础')) return 'basic';
return '';
},
getScoreCategoryDetailsByList(details, categoryCode) {
return (details || []).filter(item => this.getScoreDetailCategoryCode(item) === categoryCode);
},
getScoreCategoryDetails(categoryCode) {
return this.getScoreCategoryDetailsByList(this.currentScore.details || [], categoryCode);
},
isScoreCategoryExpanded(categoryCode) {
return this.expandedScoreCategoryKeys.includes(categoryCode);
},
toggleScoreCategory(row) {
const categoryCode = row.categoryCode;
this.expandedScoreCategoryKeys = this.isScoreCategoryExpanded(categoryCode)
? []
: [categoryCode];
},
handleScoreCategoryExpandChange(row, expandedRows) {
const expandedCategoryCodes = (expandedRows || []).map(item => item.categoryCode);
this.expandedScoreCategoryKeys = expandedCategoryCodes.includes(row.categoryCode)
? [row.categoryCode]
: [];
},
parseScoreOptions(value) {
if (!value) return [];
const normalize = list =>
list.map(item => {
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}${
this.isBlankScore(rawScore) ? '' : rawScore
}分`
: '';
const label = item.label || item.optionName || item.value || generatedLabel;
return {
...item,
label,
value: item.value ?? item.optionName ?? item.label ?? generatedLabel,
score:
item.scoreType === 'subtract' && !this.isBlankScore(rawScore)
? -Math.abs(Number(rawScore))
: rawScore,
};
});
if (Array.isArray(value)) return normalize(value);
try {
const options = JSON.parse(value);
return Array.isArray(options) ? normalize(options) : [];
} catch (error) {
return [];
}
},
isScoreTypeDetail(detail = {}) {
return String(detail.optionType || '').toLowerCase() === 'score';
},
getScoreRule(detail = {}) {
return this.parseScoreOptions(detail.optionsJson)[0] || null;
},
getScoreTypeCalculatedScore(detail = {}) {
const rule = this.getScoreRule(detail);
const base = Number(detail.baseValue);
const input = Number(detail.scoreInput);
if (!rule || !Number.isFinite(base) || !Number.isFinite(input)) return null;
const increase = rule.changeType !== 'decrease';
const valid = increase ? input >= base : input <= base;
if (!valid) return null;
const distance = increase ? input - base : base - input;
const changeValue = Number(rule.changeValue);
const stepSize = Number.isFinite(changeValue) && changeValue > 0 ? changeValue : 1;
const steps = Math.floor(distance / stepSize + 1e-9);
const stepScore = Math.abs(Number(rule.score || 0));
if (!Number.isFinite(stepScore) || steps < 0) return null;
// 减分项:按阶梯累计扣分,存正数,总分计算时再减;不做项目满分截断
if (this.isMinusScoreDetail(detail)) {
return Number((steps * stepScore).toFixed(2));
}
// 基础/加分项:从项目分值起步,按阶梯加减,限制在 [0, 项目满分]
const baseScore = Number(detail.score || 0);
const signedStep = rule.scoreType === 'subtract' ? -stepScore : stepScore;
const calculatedScore = baseScore + steps * signedStep;
const projectScore = this.getScoreItemFullScore(detail);
const upper = Math.max(projectScore, baseScore, 0);
return Number(Math.min(Math.max(calculatedScore, 0), upper).toFixed(2));
},
handleScoreValueInput(detail, value) {
const raw = String(value ?? '')
.replace(/[^\d.-]/g, '')
.replace(/(?!^)-/g, '')
.replace(/(\..*)\./g, '$1');
const sign = raw.startsWith('-') ? '-' : '';
const unsigned = raw.replace(/^-/, '');
const hasDecimal = unsigned.includes('.');
const [integerPart, decimalPart = ''] = unsigned.split('.');
const normalized = `${sign}${integerPart}${hasDecimal ? `.${decimalPart.slice(0, 2)}` : ''}`;
detail.scoreInput = normalized;
const rule = this.getScoreRule(detail);
if (!normalized || normalized === '-' || normalized === '.') {
detail.selfScore = '';
this.calculateScore(this.currentScore);
return;
}
const base = Number(detail.baseValue);
const input = Number(normalized);
if (!rule || !Number.isFinite(base) || !Number.isFinite(input)) {
detail.selfScore = '';
this.calculateScore(this.currentScore);
return;
}
const increase = rule.changeType !== 'decrease';
const valid = increase ? input >= base : input <= base;
if (!valid) {
detail.selfScore = '';
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 rawScore = option?.score;
detail.selfScore = this.isBlankScore(rawScore) ? 0 : Number(rawScore);
this.calculateScore(this.currentScore);
},
handleScoreDetailScoreInput(detail, prop, value) {
const normalized = String(value ?? '')
.replace(/[^\d.]/g, '')
.replace(/^\./, '')
.replace(/(\..*)\./g, '$1')
.replace(/^(\d+)(\.\d{0,2})?.*$/, '$1$2');
// 减分项复评得分存正数扣分,允许填 0(不扣分);不做项目满分截断
if (prop === 'reviewScore' && normalized !== '' && !this.isMinusScoreDetail(detail)) {
const maxScore = this.getScoreItemFullScore(detail);
if (Number(normalized) > maxScore) {
detail[prop] = String(maxScore);
this.$message.warning(
`${detail.itemName || '评分项目'}复评得分不能高于配置的最高得分${this.formatScoreValue(
maxScore
)}分`
);
this.calculateScore(this.currentScore);
if (prop === 'reviewScore') this.logReviewScoreSummary();
return;
}
}
detail[prop] = normalized;
this.calculateScore(this.currentScore);
if (prop === 'reviewScore') this.logReviewScoreSummary();
},
handleScoreDecimalInput(prop, value) {
const normalized = String(value || '')
.replace(/[^\d.]/g, '')
.replace(/^\./, '')
.replace(/(\..*)\./g, '$1')
.replace(/^(\d+)(\.\d{0,2})?.*$/, '$1$2');
this.currentScore[prop] = normalized;
},
handleScoreAttachmentUploadSuccess(file) {
const index = this.scoreAttachmentFiles.findIndex(
item => item.uid === file.uid || item.url === file.url
);
const attachment = {
...file,
size: file.size || '',
uploadUserName: this.userInfo.realName || this.userInfo.userName || '',
uploadTime: this.$dayjs().format('YYYY-MM-DD HH:mm:ss'),
};
if (index > -1) {
this.scoreAttachmentFiles.splice(index, 1, {
...this.scoreAttachmentFiles[index],
...attachment,
});
return;
}
this.scoreAttachmentFiles.push(attachment);
},
isMinusScoreDetail(detail = {}) {
return this.getScoreDetailCategoryCode(detail) === 'minus';
},
isBasicScoreDetail(detail = {}) {
return this.getScoreDetailCategoryCode(detail) === 'basic';
},
getSignedScore(detail, prop) {
if (this.isBlankScore(detail[prop])) return '';
const score = Number(detail[prop]);
if (!Number.isFinite(score)) return '';
return this.isMinusScoreDetail(detail) && score > 0 ? -score : score;
},
getScoreItemFullScore(detail = {}) {
const itemScore = Number(detail.score);
if (Number.isFinite(itemScore) && itemScore > 0) return itemScore;
const optionScores = this.parseScoreOptions(detail.optionsJson)
.map(item => Number(item.score))
.filter(score => Number.isFinite(score));
if (!optionScores.length) return 0;
// 减分项选项分常为负(如 -5),项目满分按绝对值取最大可扣分;避免 0 分被误判为高于“最高得分-5”
if (this.isMinusScoreDetail(detail)) {
return Math.max(...optionScores.map(score => Math.abs(score)));
}
return Math.max(...optionScores);
},
getScoreFullMark(details = []) {
return details.reduce((sum, item) => {
if (!this.isBasicScoreDetail(item)) return sum;
return sum + this.getScoreItemFullScore(item);
}, 0);
},
logReviewScoreSummary() {
const details = this.currentScore.details || [];
console.log('[评分详情] 复评评分结果', {
复评总分: this.currentScore.reviewScore,
总分: this.getScoreFullMark(details),
得分率: `${this.currentScore.scoreRate}%`,
匹配到的信用等级: this.currentScore.creditLevel || '',
});
},
matchCreditStandard(score, scoreRate) {
// 闭开区间:≥lower 且 <upper;最高档 upper=100 时包含 100(≥90 A;80≤x<90 B;…;x<50 F)
const standards = (score.standards || [])
.filter(item => item && item.creditLevel)
.sort((a, b) => Number(b.scoreRateLower || 0) - Number(a.scoreRateLower || 0));
return standards.find(standard => {
const lower = Number(standard.scoreRateLower);
const upper = Number(standard.scoreRateUpper);
if (!Number.isFinite(lower) || !Number.isFinite(upper)) return false;
if (upper >= 100) {
return scoreRate >= lower && scoreRate <= upper;
}
return scoreRate >= lower && scoreRate < upper;
});
},
calculateCreditLimitByStandard(standard, scoreRate) {
if (!standard) return '';
const lowerLimit = Number(standard.creditLimitLower);
const upperLimit = Number(standard.creditLimitUpper);
if (!Number.isFinite(lowerLimit) || !Number.isFinite(upperLimit)) return '';
const scoreRateIncrease = Number(standard.scoreRateIncrease || 0);
const creditLimitIncrease = Number(standard.creditLimitIncrease || 0);
if (scoreRateIncrease <= 0 || creditLimitIncrease <= 0) {
return Number(Math.min(lowerLimit, upperLimit).toFixed(2));
}
const steps = Math.max(
0,
Math.floor((scoreRate - Number(standard.scoreRateLower || 0)) / scoreRateIncrease)
);
const limit = lowerLimit + steps * creditLimitIncrease;
return Number(Math.min(limit, upperLimit).toFixed(2));
},
calculateScore(score) {
const details = score.details || [];
details.forEach(detail => {
if (!this.isScoreTypeDetail(detail) || String(detail.scoreInput ?? '').trim() === '')
return;
const calculatedScore = this.getScoreTypeCalculatedScore(detail);
if (calculatedScore !== null) detail.selfScore = calculatedScore;
});
const basicDetails = this.getScoreCategoryDetailsByList(details, 'basic');
const plusDetails = this.getScoreCategoryDetailsByList(details, 'plus');
const minusDetails = this.getScoreCategoryDetailsByList(details, 'minus');
const sumScore = (list, prop) =>
list.reduce((sum, item) => {
if (this.isBlankScore(item[prop])) return sum;
const value = Number(item[prop]);
return sum + (Number.isFinite(value) ? value : 0);
}, 0);
const rawPlusSelf = sumScore(plusDetails, 'selfScore');
const rawPlusReview = sumScore(plusDetails, 'reviewScore');
const rawMinusSelf = sumScore(minusDetails, 'selfScore');
const rawMinusReview = sumScore(minusDetails, 'reviewScore');
// 暂定:加分/减分合计最高按 15 分计(未超按实际,超 15 按 15/-15)
const plusSelf = Math.min(rawPlusSelf, HARDCODED_MAX_PLUS_SCORE);
const plusReview = Math.min(rawPlusReview, HARDCODED_MAX_PLUS_SCORE);
const minusSelf = Math.min(rawMinusSelf, HARDCODED_MAX_MINUS_SCORE);
const minusReview = Math.min(rawMinusReview, HARDCODED_MAX_MINUS_SCORE);
const computedSelfScore = sumScore(basicDetails, 'selfScore') + plusSelf - minusSelf;
const reviewScore = sumScore(basicDetails, 'reviewScore') + plusReview - minusReview;
const hasReviewScore = details.some(
item => !this.isBlankScore(item.reviewScore)
);
// 线下模式允许手工改自评得分:有手工值时优先采用,否则回退明细合计;-1 视为未填
const isOffline = String(score.evaluateMode || '') === 'offline';
const hasManualSelfScore = isOffline && !this.isBlankScore(score.selfScore);
const hasFilledDetail = details.some(item => {
if (this.isScoreTypeDetail(item)) {
const input = String(item.scoreInput ?? '').trim();
return input !== '' && input !== '-' && input !== '.';
}
return !this.isBlankScore(item.selectedOption);
});
const manualSelfScore = Number(score.selfScore);
const selfScore =
hasManualSelfScore && Number.isFinite(manualSelfScore) ? manualSelfScore : computedSelfScore;
const finalScore = hasReviewScore ? reviewScore : selfScore;
const fullMark = this.getScoreFullMark(details);
const scoreRate =
fullMark > 0 ? Math.max(0, Math.min(100, (finalScore / fullMark) * 100)) : 0;
const standard = this.matchCreditStandard(score, scoreRate);
const hasStandards = (score.standards || []).some(item => item && item.creditLevel);
// 线下未手工填写且明细未实际填报时保留空,避免默认写出 -1/0
const keepOfflineScoreBlank = isOffline && !hasManualSelfScore && !hasFilledDetail;
score.selfScore = keepOfflineScoreBlank ? '' : Number(Number(selfScore).toFixed(2));
score.reviewScore = Number(reviewScore.toFixed(2));
score.finalScore =
keepOfflineScoreBlank && !hasReviewScore ? '' : Number(Number(finalScore).toFixed(2));
score.scoreRate = keepOfflineScoreBlank && !hasReviewScore ? '' : Number(scoreRate.toFixed(2));
if (hasStandards) {
score.creditLevel = keepOfflineScoreBlank && !hasReviewScore ? '' : standard?.creditLevel || '';
score.maxCreditLimit =
keepOfflineScoreBlank && !hasReviewScore
? ''
: standard
? this.calculateCreditLimitByStandard(standard, scoreRate)
: '';
}
return score;
},
isBlankScore(value) {
return (
value === undefined ||
value === null ||
value === -1 ||
value === '-1' ||
String(value).trim() === ''
);
},
hasIncompleteBasicScoreDetail(details = []) {
return this.getScoreCategoryDetailsByList(details, 'basic').some(item => {
if (this.isScoreTypeDetail(item)) {
const input = String(item.scoreInput ?? '').trim();
const inputBlank = input === '' || input === '-' || input === '.';
return inputBlank || !Number.isFinite(Number(input)) || this.isBlankScore(item.selfScore);
}
return this.isBlankScore(item.selectedOption);
});
},
finishScore(score) {
if (!score.details || !score.details.length) {
this.$message.warning('请先读取评分量化表');
return;
}
if (this.hasIncompleteBasicScoreDetail(score.details)) {
this.$message.warning('请完整填写基础得分项评分明细');
return;
}
this.calculateScore(score);
score.selfStatus = '已完成';
score.reviewStatus = '已完成';
this.$message.success('评分已完成');
},
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) {
this.archiveForm.scores.splice(this.scoreRecordIndex, 1, score);
} else {
this.archiveForm.scores.push(score);
this.handleScoreCurrentChange(1);
}
this.syncArchiveCreditFromScores();
this.activeTab = 'scores';
this.scoreBox = false;
const pending = this.saveScoreArchive(successMessage);
if (this.reviewMode && successMessage && pending && pending.then) {
pending.then(() => this.closeArchive());
}
return pending;
},
deleteScore(index) {
const freezeCredit = this.shouldFreezeEffectiveCredit();
const confirmText = freezeCredit
? '确定删除该评分记录?删除后可重新添加评分表;原评分数据会保留,新评分再次审核通过前,客商等级与额度仍按原评分暂时生效。'
: '确定删除该评分记录?';
this.$confirm(confirmText, '提示', {
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;
}
if (!freezeCredit) {
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(
freezeCredit
? '评分记录已删除,原等级与额度暂时继续生效'
: '评分记录已删除',
'评分记录已删除,请保存客商档案后生效'
);
})
.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('复评确认成功');
},
validateScoreForm(requireComplete) {
if (!this.currentScore.scoreDate) {
this.$message.warning('请选择评定日期');
return false;
}
if (!this.currentScore.nextEvaluateDate) {
this.$message.warning('请选择下次评定日期');
return false;
}
if (!this.currentScore.applyCreditLimit) {
this.$message.warning('请输入拟申请总资金使用额度');
return false;
}
if (Number(this.currentScore.applyCreditLimit) < 0) {
this.$message.warning('拟申请总资金使用额度不能小于0');
return false;
}
if (!this.currentScore.quantificationId) {
this.$message.warning('请选择评分量化表');
return false;
}
if (!this.currentScore.details || !this.currentScore.details.length) {
this.$message.warning(
this.isOfflineScore
? '请先上传评分表 Excel 解析回填明细'
: '当前评分量化表未配置评分项目'
);
return false;
}
const invalidScoreInput = this.currentScore.details.find(item => {
if (!this.isScoreTypeDetail(item)) return false;
const input = String(item.scoreInput ?? '').trim();
return input !== '' && this.getScoreTypeCalculatedScore(item) === null;
});
if (invalidScoreInput) {
this.$message.warning(
`${invalidScoreInput.itemName || '评分项目'}的输入值不符合基准数值限制,请重新填写`
);
return false;
}
// 减分项允许填 0(不扣分),且按阶梯累计扣分不做项目满分截断
const overMaxReviewScore = this.currentScore.details.find(item => {
if (this.isMinusScoreDetail(item)) return false;
if (
item.reviewScore === undefined ||
item.reviewScore === null ||
String(item.reviewScore).trim() === ''
) {
return false;
}
return Number(item.reviewScore) > this.getScoreItemFullScore(item);
});
if (overMaxReviewScore) {
this.$message.warning(
`${
overMaxReviewScore.itemName || '评分项目'
}复评得分不能高于配置的最高得分${this.formatScoreValue(
this.getScoreItemFullScore(overMaxReviewScore)
)}分`
);
return false;
}
if (requireComplete && this.hasIncompleteBasicScoreDetail(this.currentScore.details)) {
this.$message.warning('请完整填写所有基础项目后再提交自评');
return false;
}
return true;
},
handleScoreCurrentChange(currentPage) {
const page = Number(currentPage) || 1;
this.scorePage.currentPage = Math.min(Math.max(page, 1), this.scorePageCount);
},
handleScoreSizeChange(pageSize) {
this.scorePage.pageSize = Number(pageSize) || 3;
this.handleScoreCurrentChange(1);
},
formatScoreValue(value) {
return value === undefined || value === null || value === '' || value === -1 || value === '-1'
? ''
: value;
},
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;
}
if (this.selectionList.some(item => item.approvalStatus !== 'draft')) {
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.selectionClear();
});
},
handleSubmitApproval(row) {
getDetail(row.id).then(res => {
const archive = this.normalizeDetail(res.data.data);
archive.qualificationAttachments = archive.qualificationAttachments || '';
if (!this.validateContactsRequired(archive)) return;
if (!this.validateScoreForApproval(archive)) return;
this.$confirm('是否提交客商准入审批?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() =>
this.submitCustomerApproval(
row.id,
archive.fullName || row.fullName,
archive.approvalStatus || row.approvalStatus
)
)
.then(() => {
this.onLoad(this.page);
this.$message({ type: 'success', message: '提交成功!' });
});
});
},
handleWithdrawApproval(row) {
this.$confirm('是否确认撤回该客商审批?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => withdrawApproval(row.id))
.then(() => {
this.onLoad(this.page);
this.$message({ type: 'success', message: '已撤回至草稿状态!' });
});
},
handleMkAudit(row) {
if (!row?.id) {
this.$message.warning('客商ID为空,无法打开审核页');
return;
}
getMKApprovalUrl({ bizId: String(row.id) })
.then(res => {
const url = res?.data?.data;
if (!url) {
this.$message.error('获取审核跳转地址失败');
return;
}
window.open(url, '_blank');
})
.catch(() => {
this.$message.error('获取审核跳转地址失败');
});
},
async handleApprove(row) {
await this.$confirm('是否确认审核通过?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
const res = await approve(row.id);
// data=false 表示审核已通过但推送金蝶失败,msg 带具体原因
if (res.data.data === false) {
this.$message.warning(res.data.msg);
} else {
this.$message.success(res.data.msg || '审核通过!');
}
this.onLoad(this.page);
},
async handleSyncKingdee(row) {
if (this.kingdeeSyncingId) return;
try {
await this.$confirm('是否确认推送金蝶?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
});
} catch (exception) {
return;
}
this.kingdeeSyncingId = row.id;
try {
const res = await syncKingdee(row.id);
const archive = res?.data?.data || {};
if (archive.kingdeeStatus === 'synced') {
this.$message.success('推送成功,金蝶客户编码与客商编号一致');
} else if (archive.kingdeeStatus === 'exist') {
this.$message.info('金蝶已有该客商档案,无需推送');
} else {
this.$message.warning(`推送失败:${archive.kingdeeFailReason || '未知原因'}`);
}
this.onLoad(this.page);
} finally {
this.kingdeeSyncingId = null;
}
},
handleReject(row) {
this.$confirm('是否确认审核不通过?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => reject(row.id))
.then(() => {
this.onLoad(this.page);
this.$message({ type: 'success', message: '操作成功!' });
});
},
handleStatus(row) {
const nextStatus = row.status === 1 ? 2 : 1;
const actionName = nextStatus === 1 ? '启用' : '停用';
this.$confirm(`是否确认${actionName}该客商?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => changeStatus(row.id, nextStatus))
.then(() => {
this.onLoad(this.page);
this.$message({ type: 'success', message: '操作成功!' });
});
},
searchReset() {
this.query = { expireStatus: this.query.expireStatus || 'all' };
this.onLoad(this.page, this.query);
},
searchChange(params, done) {
// Avue 只回传非空筛选项,被清空的字段不会出现在 params 里,
// 因此查询条件必须整体重建,不能与上一次的 this.query 合并,否则清空后仍会带上次的值
const expireStatus = this.query.expireStatus || 'all';
this.query = this.buildQuery(params, true);
this.query.expireStatus = expireStatus;
this.page.currentPage = 1;
this.onLoad(this.page, this.query);
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 = {}) {
if (!this.deptSearchInitialized) return;
this.loading = true;
getList(page.currentPage, page.pageSize, this.buildQuery(params))
.then(res => {
const pageData = res.data.data;
this.page.total = pageData.total;
this.data = pageData.records;
this.selectionClear();
})
.finally(() => {
this.loading = false;
});
},
// 黑名单判定:仅以金蝶黑名单(A801-5)命中为准(失信类别/列入事由任一非空);C101-1 的 hcsBlackStatus 不作为黑名单依据
customerRowIsBlack(row) {
return isKingdeeBlack(row.kingdeeBlackCategory, row.kingdeeBlackReason);
},
// 客商风险名称(每日定时刷新落库字段):等级与黑名单组合展示(如"低风险、黑名单");内部组织默认无风险;未查询显示"-"
customerRowRiskName(row) {
return resolveRiskTagName(row.hcsRiskLevel, this.customerRowIsBlack(row), row.customerKind);
},
// 高风险/黑名单判定(与列表筛选口径一致,命中显示红色标签)
customerRowRisky(row) {
return isRiskyTag(row.hcsRiskLevel, this.customerRowIsBlack(row));
},
// 客商名称风险校验(C101-1,仅展示不作为安全边界):名称失焦/回显时查询,
// risky 在名称框下展示红色提示,悬浮见风险项明细;查询失败静默不阻断填写,提交链路由后端强校验兜底
// 清空客商名称下风险提示(信用代码变化后原按旧编号+信用代码查询的结果失效)
clearArchiveNameRisk() {
this.archiveNameRisky = false;
this.archiveNameRiskTip = '';
this.archiveNameRiskLabel = '';
},
checkArchiveNameRisk() {
this.clearArchiveNameRisk();
const customerName = String(this.archiveForm.fullName || '').trim();
if (!customerName) return;
// 新增客商档案本地反查不到编号:金蝶带出后表单已有客商编号,优先按编号查 C101-1,
// 并传信用代码供金蝶黑名单(A801-5)兜底;无编号时按名称反查档案
const customerCode = String(this.archiveForm.customerCode || '').trim();
const request = customerCode
? getRisk(customerCode, String(this.archiveForm.unifiedCreditCode || '').trim(), customerName)
: getRiskByName(customerName);
request
.then(res => {
const risk = (res.data && res.data.data) || {};
this.archiveNameRisky = risk.risky === true;
this.archiveNameRiskTip = risk.riskTip || '';
// 红字与列表标签同口径(risk-display):等级按合同系统返回值翻译,黑名单按金蝶命中标记独立展示,组合不分家
this.archiveNameRiskLabel = this.archiveNameRisky
? (resolveRiskTagName(risk.fdRiskLevel, risk.kingdeeBlackHit === true) || '高风险/黑名单') + '客商'
: '';
})
.catch(() => {
// 忽略风险查询异常,表单正常填写
});
},
handleExport() {
this.$confirm('是否导出客商档案数据?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
NProgress.start();
exportBlob(
'/blade-transport/customer-archive/export-customer-archive',
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,
[this.website.tokenHeader]: getToken(),
};
},
// replace=true 表示以本次搜索条件为准整体重建(清空字段不会回传,需丢弃旧条件);
// 翻页、刷新等场景传 false,沿用上一次的查询条件
buildQuery(params = {}, replace = false) {
const merged = replace ? { ...params } : { ...this.query, ...params };
Object.keys(params).forEach(key => {
const value = params[key];
if (
value === '' ||
value === null ||
value === undefined ||
(Array.isArray(value) && value.length === 0)
) {
delete merged[key];
}
});
if (Array.isArray(merged.deptName)) {
const path = merged.deptName.filter(
item => item !== undefined && item !== null && item !== ''
);
const deptId = path.length ? path[path.length - 1] : '';
merged.deptName = deptId
? this.deptOptions.find(item => String(item.value) === String(deptId))?.rawLabel || ''
: '';
if (!merged.deptName) {
delete merged.deptName;
}
}
const values = normalizeSearchRangeParams(merged, createTimeRangeMap);
// 状态下拉「全部」会被数字类型转成 0/-1,二次搜索会把列表滤空。
const status = values.status;
if (status === 0 || status === '0' || status === -1 || status === '-1') {
delete values.status;
}
if (!values.expireStatus) {
values.expireStatus = this.query.expireStatus || 'all';
}
if (values.expireStatus === 'all') {
delete values.expireStatus;
}
return values;
},
handleEvaluateModeChange(mode) {
this.currentScore.evaluateMode = mode === 'offline' ? 'offline' : 'online';
if (this.isOfflineScore) {
this.expandedScoreCategoryKeys = [];
// 切到线下且明细尚未实际填报时清空自评,避免沿用线上合计或 -1 哨兵值
const hasFilledDetail = (this.currentScore.details || []).some(item => {
if (this.isScoreTypeDetail(item)) {
const input = String(item.scoreInput ?? '').trim();
return input !== '' && input !== '-' && input !== '.';
}
return !this.isBlankScore(item.selectedOption);
});
if (!hasFilledDetail || this.isBlankScore(this.currentScore.selfScore)) {
this.currentScore.selfScore = '';
}
if (!hasFilledDetail || this.isBlankScore(this.currentScore.finalScore)) {
this.currentScore.finalScore = '';
}
if (!hasFilledDetail) {
this.currentScore.scoreRate = '';
this.currentScore.creditLevel = '';
this.currentScore.maxCreditLimit = '';
}
} else {
this.calculateScore(this.currentScore);
}
},
handleOfflineSelfScoreInput(value) {
const normalized = String(value || '')
.replace(/[^\d.-]/g, '')
.replace(/(?!^)-/g, '')
.replace(/^\./, '')
.replace(/(\..*)\./g, '$1')
.replace(/^(-?\d+)(\.\d{0,2})?.*$/, '$1$2');
this.currentScore.selfScore = normalized;
},
recalculateOfflineScore() {
if (!this.isOfflineScore) return;
if (this.isBlankScore(this.currentScore.selfScore)) {
this.currentScore.selfScore = '';
const hasReviewScore = (this.currentScore.details || []).some(
item => !this.isBlankScore(item.reviewScore)
);
if (!hasReviewScore) {
this.currentScore.finalScore = '';
}
this.currentScore.scoreRate = '';
this.currentScore.creditLevel = '';
this.currentScore.maxCreditLimit = '';
return;
}
const selfScore = Number(this.currentScore.selfScore);
if (!Number.isFinite(selfScore)) return;
this.currentScore.selfScore = Number(selfScore.toFixed(2));
const hasReviewScore = (this.currentScore.details || []).some(
item => !this.isBlankScore(item.reviewScore)
);
if (!hasReviewScore) {
this.currentScore.finalScore = this.currentScore.selfScore;
}
const fullMark = this.getScoreFullMark(this.currentScore.details || []);
const scoreRate =
fullMark > 0
? Math.max(0, Math.min(100, (Number(this.currentScore.finalScore) / fullMark) * 100))
: 0;
this.currentScore.scoreRate = Number(scoreRate.toFixed(2));
const standard = this.matchCreditStandard(this.currentScore, scoreRate);
const hasStandards = (this.currentScore.standards || []).some(item => item && item.creditLevel);
if (hasStandards) {
this.currentScore.creditLevel = standard?.creditLevel || '';
this.currentScore.maxCreditLimit = standard
? this.calculateCreditLimitByStandard(standard, scoreRate)
: '';
}
},
downloadScoreTemplate() {
if (!this.currentScore.quantificationId) {
this.$message.warning('请先选择评分量化表');
return;
}
NProgress.start();
const loading = ElLoading.service({
lock: true,
text: '模板下载中',
background: 'rgba(255, 255, 255, 0.7)',
});
downloadScoreExcelTemplate(this.currentScore.quantificationId)
.then(res => {
downloadXls(
res.data,
`客商评分明细模板${this.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`
);
})
.finally(() => {
loading.close();
NProgress.done();
});
},
async uploadScoreExcel(option) {
const file = option?.file;
if (!file) return;
if (!this.currentScore.quantificationId) {
this.$message.warning('请先选择评分量化表');
return;
}
const fileName = String(file.name || '').toLowerCase();
if (!/\.(xls|xlsx)$/.test(fileName)) {
this.$message.error('请上传 .xls,.xlsx 标准格式文件');
return;
}
this.scoreImporting = true;
NProgress.start();
const loading = ElLoading.service({
lock: true,
text: '导入中',
background: 'rgba(255, 255, 255, 0.7)',
});
try {
const res = await importScoreExcel(this.currentScore.quantificationId, file);
if (isExcelResponse(res)) {
downloadXls(
res.data,
`客商评分导入失败明细${this.$dayjs().format('YYYY-MM-DD HH:mm:ss')}.xlsx`
);
this.$message.warning('部分数据导入失败,已下载失败明细');
return;
}
const result = isBusinessJson(res?.data) ? res.data : await parseBlobJson(res?.data);
if (Number(result?.code) !== 200) {
this.$message.error(result?.msg || '导入失败');
return;
}
const imported = result?.data || {};
const keepFields = {
evaluateMode: 'offline',
scoreDate: this.currentScore.scoreDate,
nextEvaluateDate: this.currentScore.nextEvaluateDate,
applyCreditLimit: this.currentScore.applyCreditLimit,
tempApplyCreditLimit: this.currentScore.tempApplyCreditLimit,
tempCreditStartDate: this.currentScore.tempCreditStartDate,
tempCreditEndDate: this.currentScore.tempCreditEndDate,
remark: this.currentScore.remark,
attachments: this.scoreAttachmentFiles,
quantificationId: this.currentScore.quantificationId,
};
this.currentScore = this.normalizeScore({
...imported,
...keepFields,
standards: imported.standards || this.currentScore.standards || [],
details: (imported.details || []).map(detail => ({
...detail,
scoreInput: this.normalizeScoreInputValue(detail.scoreInput),
})),
});
this.calculateScore(this.currentScore);
this.$message.success('评分表解析成功,请在证明材料中上传评分表原件');
} catch (error) {
const message = await resolveImportErrorMessage(error);
this.$message.error(message);
} finally {
this.scoreImporting = false;
loading.close();
NProgress.done();
}
},
},
};
</script>
<style lang="scss" scoped>
.customer-archive-page {
:deep(.el-table th .cell),
:deep(.el-table td .cell) {
white-space: nowrap;
}
// 表头含问号图标,避免被压缩成省略号
:deep(.el-table th .cell:has(.fund-use-risk-header)) {
overflow: visible;
text-overflow: clip;
}
}
.archive-tag--deep-blue {
--el-tag-bg-color: #409eff;
--el-tag-border-color: #409eff;
--el-tag-text-color: #ffffff;
}
.archive-tag--light-blue {
--el-tag-bg-color: #eef8ff;
--el-tag-border-color: #b8e1ff;
--el-tag-text-color: #409eff;
}
.business-license-expiring {
color: var(--el-color-danger);
}
.customer-archive-page__expiry-tabs {
margin-bottom: 8px;
}
.score-offline-tip {
margin-left: 12px;
color: #909399;
font-size: 12px;
}
.score-offline-actions {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
// 风险客商悬浮提示:popper 挂在 body 下,需用 :global 约束宽度
:global(.customer-risk-popper) {
max-width: 360px;
line-height: 1.6;
}
// 客商名称框下方的高风险/黑名单提示(对齐表单错误文案形态)
.archive-name-risk {
color: var(--el-color-danger);
font-size: 12px;
line-height: 1.5;
cursor: default;
}
.fund-use-risk-none {
color: #c0c4cc;
}
.fund-use-risk-header {
display: inline-flex;
align-items: center;
gap: 4px;
white-space: nowrap;
}
.fund-use-risk-help {
color: #a8abb2;
cursor: help;
font-size: 14px;
flex: none;
}
.archive-form {
:deep(.el-form-item__label) {
flex: 0 0 calc(6em + 24px) !important;
width: calc(6em + 24px) !important;
height: auto;
white-space: normal !important;
word-break: break-all;
overflow-wrap: anywhere;
line-height: 16px;
}
// 锁定表单项 label 与控件垂直居中对齐(避免长 label 折行时视觉错位)
:deep(.el-form-item) {
align-items: center;
}
// 必填星号 * 与首行文字垂直居中
// Element Plus 的 .el-form-item__label 默认 inline-flex + flex-start,
// 多行 label 时 :before 与文字行盒高度不一致导致错位。
// 通过 padding 把内容下沉到 label 中线,让 * 视觉中心落在第一行文字中心。
:deep(.el-form-item.is-required .el-form-item__label) {
align-items: flex-start;
}
:deep(.el-form-item.is-required .el-form-item__label::before) {
display: inline-block;
height: 18px;
line-height: 18px;
margin-right: 4px;
}
:deep(.el-form-item__content > .el-input),
:deep(.el-form-item__content > .el-select),
:deep(.el-form-item__content > .el-cascader),
:deep(.el-form-item__content > .el-input-number),
:deep(.el-form-item__content > .el-date-editor) {
width: 250px;
max-width: 100%;
}
:deep(.archive-form__remark .el-textarea) {
width: 100%;
}
}
.business-term-field,
.archive-form__address {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
}
.business-term-field {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
width: 100%;
:deep(.el-date-editor) {
flex: 0 0 140px;
width: 140px;
min-width: 0;
}
.business-term-separator {
flex: 0 0 auto;
color: #606266;
}
:deep(.el-checkbox) {
flex: 0 0 auto;
margin-right: 0;
white-space: nowrap;
}
}
.invoice-bank-autocomplete {
width: 100%;
}
.archive-form__address {
:deep(.el-cascader) {
flex: 0 0 240px;
width: 240px;
min-width: 0;
}
:deep(.el-input) {
flex: 1;
width: 100%;
min-width: 0;
}
}
.section-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: flex-end;
:deep(.vehicle-attachment-upload) {
width: auto;
flex: 0 0 auto;
}
}
.qualification-missing-tip {
margin-left: 8px;
color: var(--el-color-danger);
font-size: 13px;
font-weight: 400;
}
.qualification-upload-bar {
display: flex;
align-items: center;
margin-top: 12px;
margin-bottom: 6px;
:deep(.vehicle-attachment-upload) {
flex: 1;
display: flex;
align-items: center;
min-width: 0;
:deep(.el-upload) {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
}
:deep(.el-upload__tip) {
margin-left: 30px;
color: #909399;
font-size: 13px;
line-height: 1.4;
text-align: left;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
.archive-detail-card {
margin-top: 12px;
}
.archive-tabs {
:deep(.el-tabs__header) {
margin-bottom: 0;
}
}
.tab-table-toolbar {
margin-top: 16px;
margin-bottom: 12px;
}
.score-list-table {
width: 100%;
}
.score-detail-form {
padding: 8px 36px 0;
:deep(.el-form-item) {
margin-bottom: 24px;
}
:deep(.el-form-item__label) {
flex: 0 0 108px !important;
width: 108px !important;
min-width: 108px !important;
color: #70757a;
font-size: 14px;
white-space: normal !important;
word-break: break-all;
line-height: 1.2;
}
:deep(.el-form-item__content > .el-input),
:deep(.el-form-item__content > .el-select),
:deep(.el-form-item__content > .el-date-editor) {
width: 100%;
}
}
.score-category-table {
width: calc(100% - 72px);
margin: 8px 36px 16px;
:deep(.el-table__body td) {
height: 56px;
color: #303133;
}
:deep(.el-table__body tr) {
cursor: pointer;
}
}
.score-category-table__name {
display: inline-block;
min-width: 96px;
margin-right: 10px;
font-weight: 700;
}
.score-category-table__arrow {
font-size: 18px;
font-weight: 700;
color: #303133;
transition: transform 0.2s ease;
&.is-expanded {
transform: rotate(90deg);
}
}
.score-category-table {
:deep(.score-category-table__expand-column) {
width: 0 !important;
padding: 0 !important;
border-right: 0;
.cell {
display: none;
}
}
:deep(.el-table__expanded-cell) {
padding: 0;
background-color: #fff;
}
}
.score-standard-cell {
display: flex;
flex-direction: column;
justify-content: center;
//min-height: 104px;
padding: 8px 24px;
}
.score-standard-cell__label {
margin-bottom: 12px;
color: #303133;
}
.score-detail-table--inline {
margin-top: 0;
:deep(.el-table__header th) {
height: 46px;
background-color: #f5f7fa;
color: #303133;
font-weight: 700;
}
:deep(.el-table__body td) {
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) {
width: 100%;
}
}
// 客商材料、变更记录已改用全局 <section-card> 组件,无需额外 margin-top
.score-list-table,
.contact-table,
.receipt-table,
.invoice-table {
:deep(.el-table__header th) {
background-color: #f5f7fa;
}
}
.score-list-table__actions {
display: inline-flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 8px;
}
.contact-form,
.receipt-form,
.invoice-form {
max-width: 620px;
:deep(.el-form-item) {
margin-bottom: 24px;
}
:deep(.el-input) {
width: 100%;
}
}
.invoice-form {
max-width: none;
:deep(.el-input),
:deep(.el-cascader),
:deep(.el-select),
:deep(.el-autocomplete) {
width: 100%;
}
:deep(.el-cascader .el-input),
:deep(.el-autocomplete .el-input) {
width: 100%;
}
}
.invoice-contact-table {
width: 100%;
:deep(.el-input),
:deep(.el-select),
:deep(.el-cascader) {
width: 100%;
}
:deep(.el-cascader .el-input) {
width: 100%;
}
}
.contact-form__compact-field {
:deep(.el-input),
:deep(.el-cascader) {
width: 60%;
}
}
.contact-dialog__footer-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
}
.receipt-form {
:deep(.el-input),
:deep(.el-select) {
width: 60%;
}
.receipt-bank-label {
display: inline-flex;
align-items: center;
gap: 4px;
}
.receipt-bank-help {
color: #a8abb2;
cursor: help;
font-size: 14px;
flex: none;
}
}
.receipt-dialog__footer-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
}
.invoice-dialog__footer-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
}
.score-detail-table {
width: 100%;
margin-top: 12px;
}
:deep(.score-detail-dialog .el-dialog__body) {
max-height: 74vh;
overflow: auto;
padding: 18px 20px 22px;
}
:deep(.score-detail-dialog .el-dialog__footer) {
text-align: center;
.score-detail-dialog__footer-actions {
display: flex;
align-items: center;
gap: 12px;
}
.score-detail-dialog__total {
display: flex;
flex: 1;
align-items: center;
gap: 16px;
color: #606266;
font-size: 14px;
text-align: left;
span:first-child {
color: #409eff;
font-size: 16px;
font-weight: 600;
}
}
.score-detail-dialog__footer-actions .el-button {
min-width: 160px;
height: 42px;
margin: 0;
}
}
:global(.score-detail-dialog .score-detail-dialog__footer-actions) {
display: flex;
align-items: center;
width: 100%;
gap: 12px;
}
:global(.score-detail-dialog .score-detail-dialog__total) {
display: flex;
align-items: center;
gap: 16px;
margin-right: auto;
color: #606266;
font-size: 14px;
white-space: nowrap;
}
:global(.score-detail-dialog .score-detail-dialog__total span:first-child) {
color: #409eff;
font-size: 16px;
font-weight: 600;
}
.score-material-card {
:deep(.vehicle-attachment-upload) {
width: auto;
}
:deep(.el-upload-list) {
display: none;
}
}
.score-material-table {
width: 100%;
}
.change-record-content-cell {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.change-record-content-tooltip {
max-width: 900px;
max-height: 320px;
overflow: auto;
white-space: pre-wrap;
word-break: break-word;
}
.customer-archive-page.is-public-view {
:deep(.archive-page-form .archive-form__footer) {
left: 0;
}
}
.archive-page-form {
min-height: 100%;
margin-bottom: 60px;
}
.archive-form__footer {
display: flex;
justify-content: flex-end;
.archive-page-form & {
position: fixed;
right: 0;
left: 230px;
bottom: 0;
z-index: 10;
margin: 0;
padding: 12px 24px;
border-top: 1px solid #eff1f7;
background: #fff;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
}
}
:global(.avue--collapse .archive-page-form .archive-form__footer) {
left: 60px;
}
:global(.avue-layout--horizontal .archive-page-form .archive-form__footer) {
left: 0;
}
:deep(.contact-dialog .el-dialog__body) {
max-height: 70vh;
overflow: auto;
}
:deep(.receipt-dialog .el-dialog__body) {
max-height: 70vh;
overflow: auto;
}
:deep(.invoice-dialog .el-dialog__body) {
max-height: 72vh;
overflow: auto;
}
.el-col {
margin-bottom: 0;
}
.el-form-item {
//margin-bottom: 24px !important;
}
</style>