Files
tms-erp-web/src/views/vehicle/customer-archive.vue
T
gxwebsoft 4ffceb0413 fix(vehicle): 调整客户档案地区地址布局与宽度
- 将客户档案页面“所在地区”所在列从12改为6,新增“详细地址”列宽为6
- 修改“所在地区”级联选择器宽度由250px改为240px,匹配更合理的宽度设计
- 优化地址相关表单的布局,提高信息展示的清晰度和一致性
- transportCapacity/ship 组合控件宽度对齐至250px,解决两个date-picker和select宽度不一致的问题
- 修复项目表单中el-cascader宽度未统一的问题,确保级联选择器与其他输入控件同宽
2026-09-02 15:00:40 +08:00

5282 lines
186 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 #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"
disabled
placeholder="保存后自动生成"
/>
</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="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-input
v-model="archiveForm.registeredDetailAddress"
maxlength="255"
placeholder="请输入详细地址"
/>
</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="操作" min-width="135" 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" 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" 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="invoiceType" min-width="150" />
<el-table-column label="纳税人识别号" prop="taxNo" min-width="180" />
<el-table-column label="开户行名称" prop="bankName" min-width="180" />
<el-table-column label="注册电话" prop="registeredPhone" min-width="150" />
<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="邮箱" prop="email" min-width="180" />
<el-table-column label="操作" width="130" 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 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"
>
<section-card>
<el-form
ref="contactForm"
:model="contactForm"
:rules="contactRules"
label-position="right"
label-width="110px"
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="detailAddress">
<div class="contact-form__address">
<el-input
v-model="contactForm.regionName"
readonly
maxlength="100"
placeholder="行政区划自动带出"
/>
<el-input
v-model="contactForm.detailAddress"
readonly
maxlength="255"
placeholder="点击后弹出地图组件"
suffix-icon="el-icon-location"
@click="handleContactMapPick"
/>
</div>
</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="500"
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="地图选择行政区划"
append-to-body
v-model="contactMapBox"
width="920px"
class="contact-form__map-dialog"
@opened="initContactAmap"
>
<div class="contact-form__map-toolbar">
<el-input
v-model="contactMapKeyword"
clearable
placeholder="输入地址关键词"
@keyup.enter="searchContactMapKeyword"
/>
<el-button
type="primary"
icon="el-icon-search"
:loading="contactMapLoading"
@click="searchContactMapKeyword"
>
搜索
</el-button>
</div>
<div ref="contactAmap" class="contact-form__map"></div>
<div class="contact-form__map-info">
<span>{{ contactMapStatus }}</span>
<span v-if="contactMapSelected.regionName">
行政区划:{{ contactMapSelected.regionName }}
</span>
</div>
<template #footer>
<el-button @click="contactMapBox = false">取消</el-button>
<el-button
type="primary"
:disabled="!contactMapSelected.longitude"
@click="confirmContactMapPick"
>
确定
</el-button>
</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" />
</el-form-item>
<el-form-item label="注册电话" prop="registeredPhone">
<el-input v-model="invoiceForm.registeredPhone" maxlength="20" />
</el-form-item>
<el-form-item label="注册地址" prop="registeredRegionName">
<el-input
v-model="invoiceForm.registeredRegionName"
readonly
maxlength="100"
placeholder="点击后弹出地图组件"
suffix-icon="el-icon-location"
@click="handleInvoiceRegisteredMapPick"
/>
</el-form-item>
<el-form-item label="详细地址" prop="registeredDetailAddress">
<el-input
v-model="invoiceForm.registeredDetailAddress"
readonly
maxlength="255"
placeholder="点击后弹出地图组件"
suffix-icon="el-icon-location"
@click="handleInvoiceRegisteredMapPick"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="收票方名称" prop="invoiceTitle">
<el-input v-model="invoiceForm.invoiceTitle" maxlength="100" />
</el-form-item>
<el-form-item label="发票类型" prop="invoiceType">
<el-select v-model="invoiceForm.invoiceType" clearable placeholder="请选择">
<el-option label="增值税专用发票" value="增值税专用发票" />
<el-option label="普通发票" value="普通发票" />
</el-select>
</el-form-item>
<el-form-item label="开户行名称" prop="bankName">
<el-input v-model="invoiceForm.bankName" maxlength="100" />
</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 title="邮寄信息">
<el-row :gutter="34">
<el-col :span="12">
<el-form-item label="收件人姓名" prop="receiverName">
<el-input v-model="invoiceForm.receiverName" maxlength="30" />
</el-form-item>
<el-form-item label="收件人地址" prop="receiverRegionName">
<div class="invoice-form__address">
<el-input
v-model="invoiceForm.receiverRegionName"
readonly
maxlength="100"
placeholder="点击后弹出地图组件"
@click="handleInvoiceReceiverMapPick"
/>
<el-input
v-model="invoiceForm.receiverDetailAddress"
maxlength="255"
placeholder="请输入详细地址"
><template #suffix
><el-icon
class="invoice-form__address-location"
@click="handleInvoiceReceiverMapPick"
><Location /></el-icon></template
></el-input>
</div>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="收件人电话" prop="receiverPhone">
<el-input v-model="invoiceForm.receiverPhone" maxlength="20" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="invoiceForm.email" maxlength="100" />
</el-form-item>
</el-col>
</el-row>
</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%"
top="3vh"
class="score-detail-dialog"
@closed="resetScoreDialog"
>
<section-card>
<el-form
ref="scoreForm"
:model="currentScore"
label-position="right"
label-width="88"
: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)"
/>
</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 />
</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)"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="临时额度申请使用期限">
<div class="score-detail-form__range">
<el-date-picker
v-model="currentScore.tempCreditStartDate"
type="date"
value-format="YYYY-MM-DD"
/>
<span>至</span>
<el-date-picker
v-model="currentScore.tempCreditEndDate"
type="date"
value-format="YYYY-MM-DD"
/>
</div>
</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, Location } 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 AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
let amapLoader;
const qualificationViewerPlugins = [
imagePlugin(),
officePlugin({ pdf: { workerSrc: pdfWorkerSrc, useFetchData: true } }),
textPlugin(),
fallbackPlugin(),
];
export default {
components: {
SectionCard,
ArrowRight,
Location,
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(),
contactMapBox: false,
contactMapLoading: false,
contactMapKeyword: '',
contactMapStatus: '可搜索地址或点击地图选点',
contactMapSelected: {},
contactMapTarget: 'contact',
contactAmap: null,
contactAmapMarker: null,
contactAmapGeocoder: null,
receiptIndex: -1,
receiptForm: this.emptyReceiptAccount(),
invoiceIndex: -1,
invoiceForm: this.emptyInvoice(),
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: [],
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: {
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' }],
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' }],
invoiceType: [{ required: true, message: '请选择发票类型', trigger: 'change' }],
registeredPhone: [{ required: true, message: '请输入注册电话', trigger: 'blur' }],
bankName: [{ required: true, message: '请输入开户行名称', trigger: 'blur' }],
registeredRegionName: [{ required: true, message: '请选择注册地址', trigger: 'blur' }],
registeredDetailAddress: [
{ required: true, message: '请输入详细地址', trigger: 'blur' },
{ max: 255, message: '详细地址最多255个字符', trigger: 'blur' },
],
bankAccount: [{ required: true, message: '请输入银行账号', trigger: 'blur' }],
receiverDetailAddress: [
{ max: 255, message: '收件人详细地址最多255个字符', trigger: 'blur' },
],
email: [{ type: 'email', 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: 'customerNature',
type: 'select',
search: true,
searchOrder: 5,
searchValue: '',
searchPlaceholder: '请输入',
minWidth: 120,
dicData: [
{ label: '全部', value: '' },
{ label: '有限公司', value: '有限公司' },
{ label: '个体工商户', value: '个体工商户' },
],
},
{
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: '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);
},
},
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 ? '编辑发票信息' : '新增发票信息';
},
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;
},
},
methods: {
hasPermission(code) {
return this.isAdmin || this.validData(this.permission[code], false);
},
emptyArchive() {
return {
accessType: 'temporary',
approvalStatus: 'draft',
status: 1,
deptId: [],
deptIds: '',
deptName: '',
customerType: [],
businessScope: [],
businessTermType: '固定期限',
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: '客户类型',
customerNature: '客户性质',
unifiedCreditCode: '统一信用代码',
deptName: '所属组织',
approvedTime: '审核通过时间',
status: '状态',
businessTermType: '营业期限类型',
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;
},
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 (JSON.stringify(leftValue) === JSON.stringify(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',
}[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: '停用' },
};
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.isEmptyChangeValue(beforeData[field]) &&
this.isEmptyChangeValue(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]),
}),
}));
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 beforeData = this.parseChangeData(row.beforeData);
const afterData = this.parseChangeData(row.afterData);
const fields = [...new Set([...Object.keys(beforeData), ...Object.keys(afterData)])];
const details = fields
.filter(
field =>
field !== '变更内容' &&
!(
this.isEmptyChangeValue(beforeData[field]) &&
this.isEmptyChangeValue(afterData[field])
)
)
.map(field => {
const scoreDiff = this.isScoreChangeField(field)
? this.formatScoreChangeDiff(beforeData[field], afterData[field])
: null;
const before = scoreDiff?.before || this.formatChangeFieldValue(field, beforeData[field]);
const after = scoreDiff?.after || this.formatChangeFieldValue(field, afterData[field]);
return `${this.getChangeFieldLabel(field)}:变更前:${before} → 变更后:${after}`;
});
const content = String(row.changeContent || '').trim();
return [content, ...details].filter(Boolean).join('');
},
emptyContact() {
return {
contactName: '',
contactPhone: '',
email: '',
regionName: '',
regionCode: '',
detailAddress: '',
longitude: '',
latitude: '',
contactAddress: '',
branchName: '',
positionName: '',
isDefault: 0,
remark: '',
};
},
emptyReceiptAccount() {
return {
accountName: '',
accountHolderName: '',
bankName: '',
bankAccount: '',
registeredPhone: '',
registeredAddress: '',
taxNo: '',
isDefault: 0,
remark: '',
};
},
emptyInvoice() {
return {
invoiceTitle: '',
invoiceType: '',
taxNo: '',
bankName: '',
registeredPhone: '',
bankAccount: '',
registeredRegionName: '',
registeredDetailAddress: '',
registeredAddress: '',
email: '',
receiverName: '',
receiverPhone: '',
receiverRegionName: '',
receiverDetailAddress: '',
receiverAddress: '',
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) {
const nodes = this.$refs.registeredRegionCascader?.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');
});
},
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 '';
},
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');
}
});
},
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('')
);
},
formatInvoiceRegisteredAddress(row = {}) {
return (
row.registeredAddress ||
[row.registeredRegionName, row.registeredDetailAddress]
.map(item => String(item || '').trim())
.filter(Boolean)
.join('')
);
},
formatInvoiceReceiverAddress(row = {}) {
const address = [row.receiverRegionName, row.receiverDetailAddress]
.map(item => String(item || '').trim())
.filter(Boolean)
.join('');
return address || row.receiverAddress || '';
},
normalizeContact(contact = {}) {
const contactAddress =
contact.contactAddress || contact.address || this.formatContactAddress(contact);
return {
...this.emptyContact(),
...contact,
contactAddress,
branchName: contact.branchName || contact.deptName || this.archiveForm.deptName || '',
};
},
openContact(row, index = -1) {
this.contactIndex = index;
this.contactForm = row
? this.normalizeContact(row)
: {
...this.emptyContact(),
branchName: this.archiveForm.deptName || '',
};
this.contactBox = true;
},
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),
});
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(() => {});
},
handleContactMapPick() {
this.contactMapTarget = 'contact';
this.contactMapKeyword = this.contactForm.detailAddress || this.contactForm.regionName || '';
this.contactMapSelected = this.buildContactMapSelection({
lng: this.contactForm.longitude,
lat: this.contactForm.latitude,
address: this.contactForm.detailAddress,
regionName: this.contactForm.regionName,
regionCode: this.contactForm.regionCode,
});
this.contactMapStatus = this.contactMapSelected.longitude
? '已加载当前选点,可重新选点'
: '可搜索地址或点击地图选点';
this.contactMapBox = true;
},
handleInvoiceRegisteredMapPick() {
this.contactMapTarget = 'invoiceRegistered';
this.contactMapKeyword =
this.invoiceForm.registeredDetailAddress || this.invoiceForm.registeredRegionName || '';
this.contactMapSelected = this.buildContactMapSelection({
address: this.invoiceForm.registeredDetailAddress,
regionName: this.invoiceForm.registeredRegionName,
});
this.contactMapStatus = '可搜索地址或点击地图选点';
this.contactMapBox = true;
},
handleInvoiceReceiverMapPick() {
this.contactMapTarget = 'invoiceReceiver';
this.contactMapKeyword =
this.invoiceForm.receiverDetailAddress || this.invoiceForm.receiverRegionName || '';
this.contactMapSelected = this.buildContactMapSelection({
address: this.invoiceForm.receiverDetailAddress,
regionName: this.invoiceForm.receiverRegionName,
});
this.contactMapStatus = '可搜索地址或点击地图选点';
this.contactMapBox = true;
},
loadAmap() {
if (window.AMap && window.AMap.Map) {
return Promise.resolve();
}
if (!amapLoader) {
amapLoader = new Promise((resolve, reject) => {
window._AMapSecurityConfig = {
securityJsCode: AMAP_SECURITY_CODE,
};
const script = document.createElement('script');
script.src = `https://webapi.amap.com/maps?v=2.0&key=${AMAP_KEY}`;
script.async = true;
script.onload = resolve;
script.onerror = () => reject(new Error('高德地图组件加载失败'));
document.body.appendChild(script);
});
}
return amapLoader;
},
initContactAmap() {
this.loadAmap()
.then(() => {
this.$nextTick(() => {
if (!this.contactAmap) {
const center = this.toLngLat(
this.contactMapSelected.longitude || 116.40769,
this.contactMapSelected.latitude || 39.89945
);
this.contactAmap = new window.AMap.Map(this.$refs.contactAmap, {
center,
zoom: this.contactMapSelected.longitude ? 14 : 11,
});
window.AMap.plugin(['AMap.ToolBar', 'AMap.Geocoder'], () => {
this.contactAmap.addControl(new window.AMap.ToolBar());
if (!this.contactAmapGeocoder) {
this.contactAmapGeocoder = new window.AMap.Geocoder();
}
});
this.contactAmap.on('click', event => this.pickContactMapPoint(event.lnglat));
} else if (typeof this.contactAmap.resize === 'function') {
this.contactAmap.resize();
}
if (this.contactMapSelected.longitude) {
this.renderContactMapMarker(
this.toLngLat(this.contactMapSelected.longitude, this.contactMapSelected.latitude)
);
} else {
this.clearContactMapMarker();
}
});
})
.catch(() => {
this.$message.error('高德地图组件加载失败,请稍后重试或重新打开弹窗');
this.contactMapBox = false;
});
},
searchContactMapKeyword() {
const keyword = String(this.contactMapKeyword || '').trim();
if (!keyword) {
this.$message.warning('请输入地址关键词');
return;
}
this.loadAmap()
.then(() => {
this.contactMapLoading = true;
this.ensureContactAmapGeocoder()
.then(() => this.runAmapGeocode('location', keyword))
.then(result => {
const point = this.resolveMapPoint(result);
if (!point) {
this.contactMapStatus = '未找到匹配地址';
this.$message.warning('地图搜索无匹配地址');
return;
}
this.pickContactMapPoint(point, keyword);
})
.catch(() => {
this.contactMapStatus = '地图搜索失败';
this.$message.error('地图搜索失败,请稍后重试');
})
.finally(() => {
this.contactMapLoading = false;
});
})
.catch(() => {
this.$message.error('高德地图组件加载失败,请稍后重试或重新打开弹窗');
});
},
pickContactMapPoint(lnglat, keyword) {
const longitude = this.getPointLng(lnglat);
const latitude = this.getPointLat(lnglat);
if (longitude === undefined || latitude === undefined) {
this.$message.warning('选点坐标无效');
return;
}
const point = this.toLngLat(longitude, latitude);
this.renderContactMapMarker(point);
this.contactMapSelected = this.buildContactMapSelection({
lng: longitude,
lat: latitude,
address: keyword,
});
this.contactMapStatus = '正在反查地址...';
this.ensureContactAmapGeocoder()
.then(() => this.runAmapGeocode('address', point))
.then(result => {
const address = this.resolveMapAddress(result);
this.contactMapSelected = {
...this.contactMapSelected,
detailAddress: address.detailAddress || this.contactMapSelected.detailAddress,
regionName: address.regionName || this.contactMapSelected.regionName,
regionCode: address.regionCode || this.contactMapSelected.regionCode,
};
this.contactMapKeyword = this.contactMapSelected.detailAddress || this.contactMapKeyword;
this.contactMapStatus = this.contactMapSelected.detailAddress || '已选点,可确认回填';
})
.catch(() => {
this.contactMapStatus = '反查地址失败';
this.$message.error('反查地址失败,请重新选点');
});
},
renderContactMapMarker(point) {
if (!this.contactAmap || !window.AMap) return;
this.clearContactMapMarker();
this.contactAmapMarker = new window.AMap.Marker({
position: point,
});
this.contactAmapMarker.setMap(this.contactAmap);
this.contactAmap.setCenter(point);
},
clearContactMapMarker() {
if (this.contactAmapMarker) {
this.contactAmapMarker.setMap(null);
this.contactAmapMarker = null;
}
},
confirmContactMapPick() {
if (!this.contactMapSelected.longitude) {
this.$message.warning('请先搜索或点击地图完成选点');
return;
}
if (this.contactMapTarget === 'invoiceRegistered') {
this.invoiceForm.registeredRegionName =
this.contactMapSelected.regionName || this.invoiceForm.registeredRegionName;
this.invoiceForm.registeredDetailAddress = this.formatMapDetailAddress(
this.contactMapSelected.detailAddress || this.invoiceForm.registeredDetailAddress,
this.invoiceForm.registeredRegionName
);
this.$refs.invoiceForm?.validateField('registeredRegionName');
this.contactMapBox = false;
return;
}
if (this.contactMapTarget === 'invoiceReceiver') {
this.invoiceForm.receiverRegionName =
this.contactMapSelected.regionName || this.invoiceForm.receiverRegionName;
this.invoiceForm.receiverDetailAddress = this.formatMapDetailAddress(
this.contactMapSelected.detailAddress || this.invoiceForm.receiverDetailAddress,
this.invoiceForm.receiverRegionName
);
this.invoiceForm.receiverAddress = this.formatInvoiceReceiverAddress(this.invoiceForm);
this.$refs.invoiceForm?.validateField('receiverRegionName');
this.contactMapBox = false;
return;
}
this.contactForm.detailAddress =
this.contactMapSelected.detailAddress || this.contactForm.detailAddress;
this.contactForm.regionName =
this.contactMapSelected.regionName || this.contactForm.regionName;
this.contactForm.regionCode =
this.contactMapSelected.regionCode || this.contactForm.regionCode;
this.contactForm.longitude = this.contactMapSelected.longitude;
this.contactForm.latitude = this.contactMapSelected.latitude;
this.$refs.contactForm?.validateField('detailAddress');
this.contactMapBox = false;
},
formatMapDetailAddress(address, regionName) {
const rawAddress = String(address || '').trim();
const rawRegionName = String(regionName || '').trim();
if (!rawAddress || !rawRegionName) return rawAddress;
return rawAddress.startsWith(rawRegionName)
? rawAddress.slice(rawRegionName.length).trim()
: rawAddress;
},
buildContactMapSelection({ lng, lat, address, regionName, regionCode }) {
const longitude = this.formatCoordinate(lng);
const latitude = this.formatCoordinate(lat);
return {
longitude,
latitude,
detailAddress: address || '',
regionName: regionName || '',
regionCode: regionCode || '',
};
},
formatCoordinate(value) {
if (value === undefined || value === null || value === '') {
return '';
}
const numberValue = Number(value);
return Number.isFinite(numberValue) ? numberValue.toFixed(6) : '';
},
toLngLat(lng, lat) {
return new window.AMap.LngLat(Number(lng), Number(lat));
},
getPointLng(point) {
if (!point) return undefined;
if (typeof point.getLng === 'function') return point.getLng();
return point.lng ?? point.lon;
},
getPointLat(point) {
if (!point) return undefined;
if (typeof point.getLat === 'function') return point.getLat();
return point.lat;
},
ensureContactAmapGeocoder() {
if (this.contactAmapGeocoder) {
return Promise.resolve(this.contactAmapGeocoder);
}
return new Promise((resolve, reject) => {
if (!window.AMap || !window.AMap.plugin) {
reject(new Error('高德地图组件未就绪'));
return;
}
window.AMap.plugin(['AMap.Geocoder'], () => {
try {
this.contactAmapGeocoder = new window.AMap.Geocoder();
resolve(this.contactAmapGeocoder);
} catch (error) {
reject(error);
}
});
});
},
runAmapGeocode(action, input) {
return new Promise((resolve, reject) => {
const timer = window.setTimeout(() => {
reject(new Error('高德地图请求超时'));
}, 10000);
const done = (status, result) => {
window.clearTimeout(timer);
if (status === 'complete' && result) {
resolve(result);
return;
}
reject(new Error('高德地图请求失败'));
};
try {
if (action === 'location') {
this.contactAmapGeocoder.getLocation(input, done);
} else {
this.contactAmapGeocoder.getAddress(input, done);
}
} catch (error) {
window.clearTimeout(timer);
reject(error);
}
});
},
resolveMapPoint(result) {
if (!result) return null;
const geocode = Array.isArray(result.geocodes) ? result.geocodes[0] : null;
if (geocode && geocode.location) return geocode.location;
if (result.location) return result.location;
if (result.lnglat) return result.lnglat;
if (result.getLng || result.lng || result.lon) return result;
return null;
},
resolveMapAddress(result = {}) {
const regeocode = result.regeocode || {};
const component = regeocode.addressComponent || {};
const city = Array.isArray(component.city) ? '' : component.city;
const regionName = [component.province, city, component.district].filter(Boolean).join('');
return {
detailAddress: regeocode.formattedAddress || '',
regionName,
regionCode: component.adcode || '',
};
},
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) ||
'';
const receiverDetailAddress =
invoice.receiverDetailAddress ||
(invoice.receiverRegionName ? '' : invoice.receiverAddress) ||
'';
return {
...this.emptyInvoice(),
...invoice,
registeredDetailAddress,
registeredAddress: this.formatInvoiceRegisteredAddress({
...invoice,
registeredDetailAddress,
}),
receiverDetailAddress,
receiverAddress: this.formatInvoiceReceiverAddress({
...invoice,
receiverDetailAddress,
}),
};
},
openInvoice(row, index = -1) {
this.invoiceIndex = index;
this.invoiceForm = row ? this.normalizeInvoice(row) : this.emptyInvoice();
this.invoiceBox = true;
},
resetInvoice() {
this.invoiceIndex = -1;
this.invoiceForm = this.emptyInvoice();
this.$refs.invoiceForm?.clearValidate();
},
saveInvoice() {
this.$refs.invoiceForm.validate(valid => {
if (!valid) return;
const invoice = this.normalizeInvoice({
...this.invoiceForm,
registeredAddress: this.formatInvoiceRegisteredAddress(this.invoiceForm),
receiverAddress: this.formatInvoiceReceiverAddress(this.invoiceForm),
});
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)
)
);
},
validateQualificationMaterials(
files = this.qualificationFiles,
customerType = this.archiveForm.customerType
) {
const missing = this.getMissingQualificationTypes(files, customerType);
if (!missing.length) return true;
this.$message.warning(`请先上传客商材料:${missing.join('、')}`);
return false;
},
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),
invoiceTaxRate: this.normalizeOptionalAmount(detail.invoiceTaxRate),
registeredCapital: this.normalizeOptionalAmount(detail.registeredCapital),
maxCreditLimit: this.normalizeOptionalAmount(detail.maxCreditLimit),
applyCreditLimit: this.normalizeOptionalAmount(detail.applyCreditLimit),
businessScope,
businessTermType: detail.businessTermType || '固定期限',
registeredRegionPath: Array.isArray(detail.registeredRegionPath)
? detail.registeredRegionPath
: [],
registeredRegionName: detail.registeredRegionName || '',
registeredDetailAddress,
contacts: (detail.contacts || []).map(item =>
this.normalizeContact({
...item,
branchName: item.branchName || item.deptName || 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 (
!this.validateQualificationMaterials(
this.parseAttachments(archive.qualificationAttachments),
archive.customerType
)
) {
return false;
}
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;
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 =>
this.normalizeContact({
...item,
contactAddress: this.formatContactAddress(item),
})
)
.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 => this.normalizeInvoice(item))
.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();
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;
}
}
.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;
}
.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) {
color: #70757a;
font-size: 15px;
white-space: normal;
word-break: break-all;
line-height: 1.4;
}
:deep(.el-input),
:deep(.el-select),
:deep(.el-date-editor) {
width: 150px;
}
}
.score-detail-form__range {
display: flex;
align-items: center;
gap: 14px;
:deep(.el-date-editor) {
flex: 1;
min-width: 0;
}
}
.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;
}
.contact-form__address {
display: flex;
gap: 10px;
width: 100%;
.el-input {
flex: 1;
min-width: 0;
}
.el-input:first-child {
flex: 0 0 38%;
}
}
.contact-form__compact-field {
:deep(.el-input) {
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;
}
.contact-form__map-toolbar {
display: flex;
gap: 10px;
margin-bottom: 12px;
.el-input {
flex: 1;
}
}
.contact-form__map {
width: 100%;
height: 460px;
border: 1px solid #dcdfe6;
}
.contact-form__map-info {
display: flex;
justify-content: space-between;
gap: 16px;
min-height: 22px;
margin-top: 10px;
color: #606266;
font-size: 13px;
}
.invoice-form__address {
display: flex;
gap: 10px;
width: 100%;
.el-input {
flex: 1;
min-width: 0;
}
.el-input:first-child {
flex: 0 0 38%;
}
}
.invoice-form__address-location {
cursor: pointer;
color: #909399;
}
.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>