Files
tms-erp-web/src/views/vehicle/customer-archive.vue
T
2026-09-16 06:30:30 +08:00

5401 lines
192 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">
<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 #customerCode="{ row }">
<el-button type="primary" link @click="viewArchive(row)">
{{ row.customerCode }}
</el-button>
</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': isBusinessLicenseExpiringSoon(row) }">
{{ formatBusinessLicenseValidPeriod(row) }}
</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 #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') &&
['draft', 'reviewing'].includes(row.approvalStatus)
"
@click="handleWithdrawApproval(row)"
>
撤回
</el-link>
<el-link
type="success"
v-if="hasPermission('customer_archive_approve') && row.approvalStatus === 'reviewing'"
@click="handleApprove(row)"
>
审核通过
</el-link>
<el-link
type="warning"
v-if="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="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="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
@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
/>
</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 />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="经营范围" prop="businessScope" required>
<el-select v-model="archiveForm.businessScope" multiple filterable clearable>
<el-option
v-for="item in businessScopeOptions"
: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="businessEndDate"
:required="archiveForm.businessTermType === '固定期限'"
:rules="businessEndDateRules"
>
<div class="business-term-field">
<el-date-picker
v-model="archiveForm.businessEndDate"
type="date"
value-format="YYYY-MM-DD"
: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="mnemonicCode">
<el-input v-model="archiveForm.mnemonicCode" maxlength="15" show-word-limit />
</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="registeredDetailAddress" 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-row>
</section-card>
<section-card title="联系信息">
<el-row :gutter="18">
<el-col :span="6">
<el-form-item label="客商负责人" prop="principal">
<el-input v-model="archiveForm.principal" maxlength="10" show-word-limit />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="联系电话" prop="contactPhone">
<el-input v-model="archiveForm.contactPhone" maxlength="20" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="所属组织" prop="deptName">
<el-cascader
v-model="deptCascaderValue"
:options="deptTree"
:props="deptCascaderProps"
clearable
filterable
placeholder="请选择 所属组织"
/>
</el-form-item>
</el-col>
<el-col :span="6"></el-col>
</el-row>
</section-card>
<section-card title="财务/信用信息">
<el-row :gutter="18">
<el-col :span="6">
<el-form-item label="注册资金(万元)" prop="registeredCapital">
<el-input
v-model="archiveForm.registeredCapital"
maxlength="18"
@input="value => handleDecimalInput('registeredCapital', value)"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="开票税点(%" prop="invoiceTaxRate">
<el-input
v-model="archiveForm.invoiceTaxRate"
maxlength="5"
@input="value => handleDecimalInput('invoiceTaxRate', value, 13)"
/>
</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="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 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="操作" width="135" fixed="right" align="center">
<template #default="{ row }">
<el-link type="primary" @click="openScore(row.__raw, row.__rawIndex)">
详情
</el-link>
</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="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">
<template #default="{ row }">
<div
class="qualification-file-name-cell"
:title="row.originalName || row.name || '-'"
>
<span
class="qualification-file-name"
:class="{ 'is-disabled': !row.url }"
@click="row.url && previewQualificationFile(row)"
>
{{ row.originalName || row.name || '-' }}
</span>
</div>
</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>
<el-dialog
v-model="changeRecordDetailVisible"
title="变更记录详情"
append-to-body
destroy-on-close
width="1100px"
top="10px"
class="change-record-detail-dialog"
>
<div v-if="changeRecordDetail" class="change-record-detail-meta">
<span>变更日期:{{ changeRecordDetail.changeTime || '-' }}</span>
<span>变更账号:{{ changeRecordDetail.changeUserName || '-' }}</span>
</div>
<el-table :data="changeRecordDetailRows" border :show-overflow-tooltip="false">
<el-table-column prop="field" label="变更字段" min-width="180" />
<el-table-column
prop="before"
label="变更前"
min-width="360"
class-name="change-record-detail-value"
/>
<el-table-column
prop="after"
label="变更后"
min-width="500"
class-name="change-record-detail-value"
/>
</el-table>
<el-empty
v-if="!changeRecordDetailRows.length"
description="暂无变更内容"
:image-size="60"
/>
<template #footer>
<el-button type="primary" @click="changeRecordDetailVisible = false">关闭</el-button>
</template>
</el-dialog>
<div class="archive-form__footer">
<!-- 次要:独立页返回 / 只读关闭 / 弹窗取消 -->
<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 label="开户行" prop="bankName">
<el-input v-model="receiptForm.bankName" maxlength="100" />
</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-input v-model="invoiceForm.bankName" maxlength="100" />
</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="200">
<template #default="{ row }">
<el-input
v-if="!isInternalCustomer"
:model-value="row.deptNames"
disabled
/>
<el-select
v-else
v-model="row.deptIdList"
multiple
collapse-tags
collapse-tags-tooltip
filterable
clearable
:disabled="readonly"
placeholder="请选择所属部门"
@change="() => handleInvoiceContactDeptChange(row)"
>
<el-option
v-for="item in invoiceDeptOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
</el-table-column>
<el-table-column label="备注" min-width="160">
<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="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-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.finalScore)" disabled />
</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 :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="临时申请额度">
<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 :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 :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="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 :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="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="评分项目" prop="itemName" width="150" align="center" />
<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)"
>
<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 label="复评得分" width="150" align="center">
<template #default="{ row: detail }">
<el-input
v-model="detail.reviewScore"
maxlength="10"
inputmode="decimal"
:disabled="readonly || !hasPermission('customer_type_re_cert')"
@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>复评:{{ 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">
<template #default="{ row }">
{{ row.originalName || row.name || '-' }}
</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>复评:{{ formatScoreValue(currentScore.reviewScore) }}分</span>
</div>
<el-button @click="scoreBox = false">取消</el-button>
<el-button type="primary" plain v-if="!readonly" @click="saveScoreDetail">保存</el-button>
<el-button
v-if="!readonly && 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,
getChangeRecordList,
submit,
submitApproval,
withdrawApproval,
approve,
reject,
changeStatus,
remove,
getScoreTemplate,
recognizeBusinessLicenseOcr,
} from '@/api/vehicle/customer-archive';
import {
getList as getCreditScoreQuantificationList,
getDetail as getCreditScoreQuantificationDetail,
} from '@/api/vehicle/credit-score-quantification';
import { getDictionary } from '@/api/system/dictbiz';
import { getDeptTree } from '@/api/system/dept';
import { getLazyTree } from '@/api/base/region';
import { exportBlob } from '@/api/common';
import { downloadFileByUrl, downloadXls } from '@/utils/util';
import { getToken } 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';
const createTimeRangeMap = {
createTimeRange: ['createTimeStart', 'createTimeEnd'],
};
const qualificationViewerPlugins = [
imagePlugin(),
officePlugin({ pdf: { workerSrc: pdfWorkerSrc, useFetchData: true } }),
textPlugin(),
fallbackPlugin(),
];
export default {
components: {
SectionCard,
ArrowRight,
ElImageViewer,
OpenFileViewer,
PdfPreview,
},
data() {
const nonNegativeLabelMap = {
invoiceTaxRate: '开票税点',
registeredCapital: '注册资金',
maxCreditLimit: '最大资金使用额度',
applyCreditLimit: '申请总资金使用额度',
};
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 validateInvoiceTaxRate = (rule, value, callback) => {
if (value === undefined || value === null || value === '') {
callback();
} else if (Number(value) < 0 || Number(value) > 13) {
callback(new Error('开票税点必须在0到13之间'));
} else {
callback();
}
};
const validateUnifiedCreditCode = (rule, value, callback) => {
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) {
callback();
return;
}
if (code.length !== 18 || ![...code].every(char => charset.includes(char))) {
callback(new Error('统一社会信用代码必须为18位有效字符'));
return;
}
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) {
callback(new Error('统一社会信用代码校验码错误'));
return;
}
callback();
};
const validateRegisteredAddress = (rule, value, callback) => {
const regionName = (this.archiveForm.registeredRegionName || '').trim();
const detailAddress = String(value || '').trim();
if (!regionName) {
callback(new Error('请选择省市区'));
} else if (!detailAddress) {
callback(new Error('请输入详细地址'));
} else if (detailAddress.length > 255) {
callback(new Error('详细地址最多255个字符'));
} else {
callback();
}
};
return {
form: {},
query: {},
deptSearchInitialized: false,
loading: true,
data: [],
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,
originalAccessType: '',
activeTab: 'scores',
archiveForm: this.emptyArchive(),
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 },
],
businessScopeOptions: [],
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' },
},
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' }],
contactPhone: [
{ required: true, message: '请输入联系电话', trigger: 'blur' },
{ pattern: /^1\d{10}$/, message: '联系电话必须为11位手机号', trigger: 'blur' },
],
deptName: [{ required: true, message: '请选择所属组织', trigger: 'change' }],
invoiceTaxRate: [{ validator: validateInvoiceTaxRate, trigger: 'blur' }],
registeredCapital: [{ validator: validateNonNegative, trigger: 'blur' }],
maxCreditLimit: [{ validator: validateNonNegative, trigger: 'blur' }],
applyCreditLimit: [{ validator: validateNonNegative, trigger: 'blur' }],
registeredDetailAddress: [
{ validator: validateRegisteredAddress, trigger: ['change', 'blur'] },
],
businessScope: [
{
required: true,
type: 'array',
min: 1,
message: '请选择经营范围',
trigger: 'change',
},
],
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' }],
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: 280,
column: [
{
label: '客商编号',
prop: 'customerCode',
slot: true,
search: true,
searchOrder: 10,
searchPlaceholder: '请输入',
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: '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: '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: 'status',
slot: true,
type: 'select',
search: true,
searchOrder: 6,
searchValue: '',
dataType: 'number',
minWidth: 100,
dicData: [
{ label: '全部', value: '' },
{ label: '启用', value: 1 },
{ label: '停用', value: 2 },
],
clearable: true,
},
],
},
};
},
created() {
this.initDeptTree();
this.initRegionOptions();
this.initBusinessDictionaries();
this.initScoreQuantificationOptions();
if (this.isArchivePage) {
const readonly = this.$route.query.view === '1' || this.$route.query.view === 'true';
this.openArchive(this.$route.query.id ? { id: this.$route.query.id } : null, readonly);
}
},
computed: {
...mapGetters(['permission', 'userInfo']),
isAdmin() {
const authority = this.userInfo.authority || '';
return authority.includes('admin');
},
permissionList() {
return {
addBtn: false,
};
},
isArchivePage() {
return this.$route.path === '/vehicle/customer-archive/form';
},
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',
};
},
// 所属组织级联值: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.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';
},
invoiceDeptOptions() {
return this.flattenDept(this.deptTree).map(item => ({
label: item.rawLabel,
value: String(item.value),
}));
},
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 || [];
},
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 selfScore = details.reduce((sum, item) => sum + Number(item.selfScore || 0), 0);
const reviewScore = details.reduce((sum, item) => sum + Number(item.reviewScore || 0), 0);
return {
...category,
index: index + 1,
selfScore: Number(selfScore.toFixed(2)),
reviewScore: Number(reviewScore.toFixed(2)),
};
});
return rows;
},
},
watch: {
'archiveForm.registeredDetailAddress'() {
this.$nextTick(() => this.checkOverflow('registeredDetailInput', 'registeredDetailOverflow'));
},
'invoiceForm.registeredDetailAddress'() {
this.$nextTick(() => this.checkOverflow('invoiceDetailInput', 'invoiceDetailOverflow'));
},
},
methods: {
hasPermission(code) {
return this.isAdmin || this.validData(this.permission[code], false);
},
// 检测输入框文本是否溢出,仅溢出时才允许 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: '固定期限',
networkFreightPlatform: null,
guangxiTop100: null,
registeredRegionPath: [],
registeredRegionName: '',
registeredDetailAddress: '',
registeredAddress: '',
contacts: [],
receiptAccounts: [],
invoices: [],
scores: [],
changeRecords: [],
};
},
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;
getChangeRecordList(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: '客商编号',
shortName: '客商简称',
fullName: '客商名称',
customerType: '客商分类',
customerKind: '客商类型',
customerNature: '客户性质',
guangxiTop100: '是否广西百强',
unifiedCreditCode: '统一信用代码',
deptName: '所属组织',
approvedTime: '审核通过时间',
status: '状态',
businessTermType: '营业期限类型',
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: '',
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('registeredDetailAddress');
return;
}
this.$nextTick(() => {
this.archiveForm.registeredRegionName = this.getRegisteredRegionLabels(value).join('');
this.$refs.archiveForm?.validateField('registeredDetailAddress');
});
},
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() {
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('scope_of_business', 'businessScopeOptions');
this.loadDictOptions('customer_attachment_types', 'qualificationTypeOptions', [], () => {
this.sortQualificationFiles();
});
},
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;
},
setDefaultTopLevelDept() {
const deptIds = Array.isArray(this.archiveForm.deptId)
? this.archiveForm.deptId
: this.archiveForm.deptId
? [this.archiveForm.deptId]
: [];
const firstTopLevelDept = this.deptTree[0];
if (!this.isArchivePage || this.$route.query.id || deptIds.length || !firstTopLevelDept) {
return;
}
this.onDeptChange([firstTopLevelDept.value]);
},
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 '无固定期限';
return row.businessEndDate || '';
},
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 = {}) {
if (row.businessTermType === '长期' || !row.businessEndDate) return false;
const endDate = this.$dayjs(row.businessEndDate);
if (!endDate.isValid()) return false;
const today = this.$dayjs().startOf('day');
const expireSoonDate = today.add(30, 'day');
return !endDate.isAfter(expireSoonDate, 'day');
},
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();
},
saveReceipt() {
this.$refs.receiptForm.validate(valid => {
if (!valid) 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) {
const ids = Array.isArray(row.deptIdList) ? row.deptIdList.map(item => String(item)) : [];
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?.result || {});
this.$message.success('营业执照识别完成');
})
.catch(() => {
this.$message.warning('营业执照上传成功,自动识别失败,请手动填写工商信息');
})
.finally(() => loading.close());
},
getBusinessLicenseOcrValue(result, labels) {
const words = result?.words_result || result?.wordsResult || result || {};
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 code = this.getBusinessLicenseOcrValue(result, [
'统一社会信用代码',
'社会信用代码',
'注册号',
]);
const name = this.getBusinessLicenseOcrValue(result, ['单位名称', '企业名称', '名称']);
const legalPerson = this.getBusinessLicenseOcrValue(result, ['法定代表人', '法人']);
const address = this.getBusinessLicenseOcrValue(result, ['地址', '住所']);
const capital = this.getBusinessLicenseOcrValue(result, ['注册资本']);
const term = this.getBusinessLicenseOcrValue(result, ['营业期限', '有效期']);
const termStartDate = this.getBusinessLicenseOcrValue(result, ['有效期起始日期']);
const scope = this.getBusinessLicenseOcrValue(result, ['经营范围']);
if (code) this.archiveForm.unifiedCreditCode = code.replace(/\s/g, '').toUpperCase();
if (name) this.archiveForm.fullName = name;
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];
}
this.applyBusinessTermRecognition(term);
if (this.isNoFixedBusinessTerm(term) || this.isNoFixedBusinessTerm(termStartDate)) {
this.handleNoFixedTermChange(true);
}
if (scope) {
const matchedScopes = this.businessScopeOptions
.filter(item => scope.includes(item.label) || scope.includes(item.value))
.map(item => item.value);
if (matchedScopes.length) this.archiveForm.businessScope = matchedScopes;
}
this.$nextTick(() => {
[
'unifiedCreditCode',
'fullName',
'legalPerson',
'registeredCapital',
'businessEndDate',
'businessScope',
'registeredDetailAddress',
].forEach(prop => this.$refs.archiveForm?.validateField(prop));
});
},
applyBusinessTermRecognition(term) {
if (!term) return;
if (/长期|永久/.test(term)) {
this.archiveForm.businessTermType = '长期';
this.archiveForm.businessEndDate = '';
return;
}
const match = term.match(/(\d{4}[年./-]\d{1,2}[月./-]\d{1,2})/);
if (!match) return;
const date = match[1]
.replace(/[年./]/g, '-')
.replace('月', '-')
.replace('日', '');
this.archiveForm.businessTermType = '固定期限';
this.archiveForm.businessEndDate = date;
},
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 businessScope = this.splitValue(detail.businessScope);
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),
maxCreditLimit: this.normalizeOptionalAmount(detail.maxCreditLimit),
applyCreditLimit: this.normalizeOptionalAmount(detail.applyCreditLimit),
businessScope,
businessTermType: detail.businessTermType || '固定期限',
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: '',
scoreDate: this.$dayjs().format('YYYY-MM-DD'),
applyCreditLimit: this.archiveForm.applyCreditLimit || '',
maxCreditLimit: '',
creditLevel: '',
standards: [],
tempApplyCreditLimit: '',
tempCreditStartDate: '',
tempCreditEndDate: '',
remark: '',
selfStatus: '未完成',
reviewStatus: '未完成',
proofAttachments: '',
attachments: [],
details: [],
...score,
quantificationId: score.quantificationId ? String(score.quantificationId) : '',
applyCreditLimit: this.normalizeOptionalAmount(
score.applyCreditLimit === undefined
? this.archiveForm.applyCreditLimit
: score.applyCreditLimit
),
maxCreditLimit: this.normalizeOptionalAmount(score.maxCreditLimit),
tempApplyCreditLimit: this.normalizeOptionalAmount(score.tempApplyCreditLimit),
attachments: score.attachments || this.parseAttachments(score.proofAttachments),
details: (score.details || []).map(detail => ({
...detail,
scoreInput: this.normalizeScoreInputValue(detail.scoreInput),
})),
};
},
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;
},
applyArchiveCreditFromScores(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: '/vehicle/customer-archive/form',
query: isEdit ? { id: row.id, name: '编辑客商档案' } : { name: '新增客商档案' },
});
},
viewArchive(row) {
this.$router.push({
path: '/vehicle/customer-archive/form',
query: { id: row.id, name: '查看客商档案', view: '1' },
});
},
closeArchive() {
this.$router.push('/vehicle/customer-archive');
},
openArchive(row, readonly = false) {
this.readonly = readonly;
this.activeTab = 'scores';
this.resetDetailPagination();
this.resetChangeRecordPage();
if (row && row.id) {
getDetail(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 = [];
this.archiveBox = true;
this.loadChangeRecords();
});
return;
}
this.archiveForm = this.emptyArchive();
this.originalAccessType = '';
this.qualificationFiles = [];
this.qualificationUploadFiles = [];
this.ocrQualificationUploadFiles = [];
this.selectedQualificationFiles = [];
this.archiveBox = true;
},
resetArchive() {
this.readonly = false;
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();
},
// 客商材料不做提交校验,仅在页面上提示未上传项
validateFormalForApproval(archive) {
if (archive.accessType !== 'formal') return true;
const hasCompletedScore = (archive.scores || []).some(
item => item.selfStatus === '已完成' || item.reviewStatus === '已完成' || item.finalScore
);
if (!hasCompletedScore) {
this.$message.warning('正式客商提交审核前必须完成信用评分');
return false;
}
return true;
},
normalizeArchive() {
const archive = JSON.parse(JSON.stringify(this.archiveForm));
if (archive.businessTermType === '长期') {
archive.businessEndDate = null;
}
const invoiceTaxRate = this.normalizeOptionalAmount(archive.invoiceTaxRate);
const registeredCapital = this.normalizeOptionalAmount(archive.registeredCapital);
archive.invoiceTaxRate = invoiceTaxRate === '' ? null : invoiceTaxRate;
archive.registeredCapital = registeredCapital === '' ? null : registeredCapital;
archive.customerType = Array.isArray(archive.customerType)
? archive.customerType.join(',')
: archive.customerType;
archive.businessScope = Array.isArray(archive.businessScope)
? archive.businessScope.join(',')
: archive.businessScope;
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);
archive.receiptAccounts = (archive.receiptAccounts || [])
.map(item => this.normalizeReceipt(item))
.filter(item => item.accountName || item.accountHolderName || item.bankAccount);
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;
},
saveArchive() {
this.$refs.archiveForm.validate(valid => {
if (!valid) {
this.$message.warning('请先完整填写必填项');
return;
}
submit(this.normalizeArchive()).then(() => {
this.closeArchive();
this.onLoad(this.page);
this.$message({ type: 'success', message: '操作成功!' });
});
});
},
saveAndSubmitArchive() {
this.$refs.archiveForm.validate(valid => {
if (!valid) {
this.$message.warning('请先完整填写必填项');
return;
}
const archive = this.normalizeArchive();
// 仅提交时落变更记录;保存不记录
archive.recordChange = true;
if (!this.validateFormalForApproval(archive)) return;
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;
}
submitApproval(id).then(() => {
this.closeArchive();
this.onLoad(this.page);
this.$message({ type: 'success', message: '提交成功!' });
});
});
});
},
addScore() {
this.scoreRecordIndex = -1;
this.currentScore = this.normalizeScore({});
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)
);
},
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,
scoreDate: this.currentScore.scoreDate,
applyCreditLimit: this.currentScore.applyCreditLimit,
maxCreditLimit: '',
creditLevel: '',
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);
});
},
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 generatedLabel = isScoreOption
? `${changeLabel}${item.changeValue || ''}${item.changeUnit || ''}${scoreLabel}${
item.score || ''
}分`
: '';
const label = item.label || item.optionName || item.value || generatedLabel;
return {
...item,
label,
value: item.value || item.optionName || item.label || generatedLabel,
score:
item.scoreType === 'subtract' && item.score !== undefined
? -Math.abs(Number(item.score))
: item.score,
};
});
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 stepScore = Math.abs(Number(rule.score || 0));
if (!Number.isFinite(stepScore)) return null;
const projectScore = this.getScoreItemFullScore(detail);
const signedStep = rule.scoreType === 'subtract' ? -stepScore : stepScore;
const calculatedScore = Number(detail.score || 0) + distance * signedStep;
return Number(Math.min(Math.max(calculatedScore, 0), projectScore).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.$message.warning(`输入值${increase ? '不能小于' : '不能大于'}基准数值${base}`);
this.calculateScore(this.currentScore);
return;
}
detail.selfScore = this.getScoreTypeCalculatedScore(detail);
this.calculateScore(this.currentScore);
},
scoreOptionChange(detail, optionValue) {
const option = this.parseScoreOptions(detail.optionsJson).find(
item => item.value === optionValue
);
const score = Number(option?.score || 0);
detail.selfScore = score;
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');
if (prop === 'reviewScore' && normalized !== '') {
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) {
const score = Number(detail[prop] || 0);
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));
return optionScores.length ? Math.max(...optionScores) : 0;
},
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) {
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;
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) => sum + Number(item[prop] || 0), 0);
const selfScore =
sumScore(basicDetails, 'selfScore') +
sumScore(plusDetails, 'selfScore') -
sumScore(minusDetails, 'selfScore');
const reviewScore =
sumScore(basicDetails, 'reviewScore') +
sumScore(plusDetails, 'reviewScore') -
sumScore(minusDetails, 'reviewScore');
const hasReviewScore = details.some(
item =>
item.reviewScore !== undefined && item.reviewScore !== null && item.reviewScore !== ''
);
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);
score.selfScore = Number(selfScore.toFixed(2));
score.reviewScore = Number(reviewScore.toFixed(2));
score.finalScore = Number(finalScore.toFixed(2));
score.scoreRate = Number(scoreRate.toFixed(2));
if (hasStandards) {
score.creditLevel = standard?.creditLevel || '';
score.maxCreditLimit = standard
? this.calculateCreditLimitByStandard(standard, scoreRate)
: '';
}
return score;
},
hasIncompleteBasicScoreDetail(details = []) {
return this.getScoreCategoryDetailsByList(details, 'basic').some(item => {
if (this.isScoreTypeDetail(item)) {
const input = String(item.scoreInput ?? '').trim();
return !input || !Number.isFinite(Number(input)) || item.selfScore === '';
}
return !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() {
if (!this.validateScoreForm(false)) return;
this.currentScore.attachments = JSON.parse(JSON.stringify(this.scoreAttachmentFiles || []));
this.currentScore.proofAttachments = this.stringifyAttachments(this.scoreAttachmentFiles);
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;
this.saveScoreArchive();
},
saveScoreArchive() {
if (!this.archiveForm.id) {
this.$message.success('评分记录已添加,请保存客商档案后生效');
return;
}
submit(this.normalizeArchive()).then(() => {
this.$message.success('评分记录已保存');
});
},
confirmReviewScore() {
if (!this.validateScoreForm(true)) return;
this.currentScore.selfStatus = '已完成';
this.currentScore.reviewStatus = '已完成';
this.saveScoreDetail();
this.$message.success('复评确认成功');
},
validateScoreForm(requireComplete) {
if (!this.currentScore.scoreDate) {
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('当前评分量化表未配置评分项目');
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;
}
const overMaxReviewScore = this.currentScore.details.find(item => {
if (
item.reviewScore === undefined ||
item.reviewScore === null ||
item.reviewScore === ''
) {
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.validateFormalForApproval(archive)) return;
this.$confirm('是否提交客商准入审批?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => submitApproval(row.id))
.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: '已撤回至草稿状态!' });
});
},
handleApprove(row) {
this.$confirm('是否确认审核通过?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => approve(row.id))
.then(() => {
this.onLoad(this.page);
this.$message({ type: 'success', message: '审核通过!' });
});
},
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 = {};
this.onLoad(this.page);
},
searchChange(params, done) {
// 搜索区所属组织级联返回的是 ID 路径数组,取末级 ID 转成部门名称传给后端(与列表接口 deptName 字段一致)
if (Array.isArray(params.deptName)) {
const path = params.deptName.filter(
item => item !== undefined && item !== null && item !== ''
);
const deptId = path.length ? path[path.length - 1] : '';
params.deptName = deptId
? this.deptOptions.find(item => String(item.value) === String(deptId))?.rawLabel || ''
: '';
}
this.query = this.buildQuery(params);
this.page.currentPage = 1;
this.onLoad(this.page);
done();
},
selectionChange(list) {
this.selectionList = list;
},
selectionClear() {
this.selectionList = [];
this.$refs.crud?.toggleSelection();
},
currentChange(currentPage) {
this.page.currentPage = currentPage;
},
sizeChange(pageSize) {
this.page.pageSize = pageSize;
},
refreshChange() {
this.onLoad(this.page, this.query);
},
onLoad(page, params = {}) {
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;
});
},
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(),
};
},
buildQuery(params = {}) {
return normalizeSearchRangeParams({ ...params, ...this.query }, createTimeRangeMap);
},
},
};
</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);
}
.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: nowrap;
:deep(.el-date-editor) {
flex: 0 0 140px;
width: 140px;
min-width: 0;
}
:deep(.el-checkbox) {
flex: 0 0 auto;
margin-right: 0;
white-space: nowrap;
}
}
.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-file-name-cell {
display: block;
width: 100%;
min-width: 0;
overflow: hidden;
text-align: left;
text-overflow: ellipsis;
white-space: nowrap;
}
.qualification-file-name {
display: block;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
white-space: nowrap;
cursor: pointer;
color: #606266;
transition: color 0.2s ease;
&:hover {
color: #409eff;
}
&.is-disabled {
cursor: default;
color: #606266;
&:hover {
color: #606266;
}
}
}
.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: 14px;
}
: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) {
//min-height: 112px;
color: #303133;
}
: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;
}
}
.contact-form,
.receipt-form,
.invoice-form {
max-width: 620px;
:deep(.el-form-item) {
margin-bottom: 14px;
}
:deep(.el-input) {
width: 100%;
}
}
.invoice-form {
max-width: none;
}
.invoice-contact-table {
width: 100%;
:deep(.el-input),
:deep(.el-select) {
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-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;
}
:deep(.change-record-detail-dialog .el-dialog__body) {
max-height: 65vh;
overflow: auto;
}
:deep(.change-record-detail-dialog .change-record-detail-value .cell) {
white-space: pre-wrap;
word-break: break-word;
overflow: visible;
text-overflow: clip;
line-height: 1.6;
}
.change-record-detail-meta {
display: flex;
gap: 32px;
margin-bottom: 16px;
color: #606266;
font-size: 14px;
}
.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: 14px !important;
}
</style>