Files
tms-erp-web/src/views/vehicle/customer-archive.vue
赵忠林 c92d2a405a style(ui): 统一操作列文字字重和全站表格文字溢出提示
- 取消操作列内 el-link 默认加粗,统一为常规字体粗细,避免字体显得突兀
- 在 element-ui.scss 中新增样式覆盖 el-link 的 font-weight 为 normal
- 全站表格开启 Element Plus 全局配置 showOverflowTooltip,统一显示单元格文字省略提示
- Avue 全局 crudOption 和手写表格均启用 showOverflowTooltip,覆盖所有相关表格列
- 通过 CSS 豁免操作列单元格不受 tooltip 固定宽度裁切影响,保证操作列内部链接显示完整
- 调整部分菜单宽度以适配视觉和布局需求
- 验证构建成功,无新增 CSS 警告,确保兼容性和功能正常
2026-08-07 12:24:17 +08:00

3893 lines
133 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
: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="openArchive()"
>新增
</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="openArchive(row, true)">
{{ row.customerCode }}
</el-button>
</template>
<template #accessType="{ row }">
<el-tag :type="row.accessType === 'formal' ? 'success' : 'warning'">
{{ accessTypeMap[row.accessType] || '临时客商' }}
</el-tag>
</template>
<template #approvalStatus="{ row }">
<el-tag :type="approvalStatusMap[row.approvalStatus]?.type || 'info'">
{{ approvalStatusMap[row.approvalStatus]?.label || '草稿' }}
</el-tag>
</template>
<template #status="{ row }">
<el-tag :type="row.status === 1 ? 'primary' : 'info'">
{{ row.status === 1 ? '启用' : '停用' }}
</el-tag>
</template>
<template #menu="{ row }">
<el-link
type="primary"
v-if="hasPermission('customer_archive_view')"
@click="openArchive(row, true)"
>
查看
</el-link>
<el-link
type="primary"
v-if="hasPermission('customer_archive_edit') && row.approvalStatus !== 'reviewing'"
@click="openArchive(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
:page="page"
@size-change="sizeChange"
@current-change="currentChange"
@load="onLoad(page, query)"
/>
<el-dialog
:title="dialogTitle"
append-to-body
v-model="archiveBox"
width="92%"
top="4vh"
class="archive-dialog"
@closed="resetArchive"
>
<el-form
ref="archiveForm"
:model="archiveForm"
:rules="formRules"
label-position="right"
label-width="auto"
:disabled="readonly"
class="archive-form"
>
<div class="section-title dialog-section-title">基本信息</div>
<div class="archive-form__group">
<div class="dialog-section-title archive-form__group-title">工商信息</div>
<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 === '固定期限'"
>
<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="24">
<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"
/>
<el-input
v-model="archiveForm.registeredDetailAddress"
maxlength="255"
placeholder="请输入详细地址"
/>
</div>
</el-form-item>
</el-col>
</el-row>
</div>
<div class="archive-form__group">
<div class="dialog-section-title archive-form__group-title">联系信息</div>
<el-row :gutter="18">
<el-col :span="8">
<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="8">
<el-form-item label="联系电话" prop="contactPhone">
<el-input v-model="archiveForm.contactPhone" maxlength="20" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="所属组织" prop="deptName">
<el-tree-select
v-model="archiveForm.deptId"
:data="deptTree"
:props="{ label: 'label', value: 'value', children: 'children' }"
multiple
node-key="value"
check-strictly
filterable
clearable
:render-after-expand="false"
@change="onDeptChange"
/>
</el-form-item>
</el-col>
</el-row>
</div>
<div class="archive-form__group">
<div class="dialog-section-title archive-form__group-title">财务/信用信息</div>
<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-row>
</div>
<div class="archive-form__group">
<div class="dialog-section-title archive-form__group-title">其他信息</div>
<el-row :gutter="18">
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input
v-model="archiveForm.remark"
type="textarea"
maxlength="500"
show-word-limit
/>
</el-form-item>
</el-col>
</el-row>
</div>
</el-form>
<el-tabs v-model="activeTab" class="archive-tabs">
<el-tab-pane label="评分表" name="scores">
<div class="section-head">
<span></span>
<el-button type="primary" icon="el-icon-plus" v-if="!readonly" @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">
<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="185" align="center">
<template #header>
<span>最大资金使用额度(万<br />元)</span>
</template>
<template #default="{ row }">
<span>{{ formatScoreValue(row.maxCreditLimit) }}</span>
</template>
</el-table-column>
<el-table-column min-width="190" align="center">
<template #header>
<span>拟申请总资金使用额<br />度(万元)</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="section-head">
<span></span>
<el-button type="primary" icon="el-icon-plus" v-if="!readonly" @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="section-head">
<span></span>
<el-button type="primary" icon="el-icon-plus" v-if="!readonly" @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="section-head">
<span></span>
<el-button type="primary" icon="el-icon-plus" v-if="!readonly" @click="openInvoice()">
新增发票信息
</el-button>
</div>
<el-table :data="invoiceTableData" border class="invoice-table">
<el-table-column label="序号" prop="__index" width="90" align="center" />
<el-table-column label="收票方名称" prop="invoiceTitle" min-width="180" />
<el-table-column label="纳税人识别号" prop="taxNo" min-width="180" />
<el-table-column label="开户行名称" prop="bankName" min-width="180" />
<el-table-column label="注册电话" prop="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>
<div class="customer-material-section">
<div class="section-head">
<div class="section-title dialog-section-title">客商材料</div>
<div v-if="!readonly" class="section-actions">
<el-button icon="el-icon-download" @click="downloadAttachments">批量下载</el-button>
<el-button type="primary" icon="el-icon-plus" @click="addAttachment"
>普通上传</el-button
>
<el-button type="primary" icon="el-icon-upload" @click="addAttachment">
OCR上传识别
</el-button>
</div>
</div>
<el-alert
type="info"
:closable="false"
title="支持pdf、bmp、jpeg、png、jpg、doc、docx、ppt、pptx、xlsx、xls、eml、msg、zip的文件格式单个文件大小限制500M"
class="archive-rule-alert"
/>
<el-alert
v-if="missingQualificationText"
type="error"
:closable="false"
:title="missingQualificationText"
class="archive-rule-alert"
/>
<el-table :data="qualificationFiles" border>
<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>
<el-option label="法人身份证" value="法人身份证" />
<el-option label="道路运输许可证" value="道路运输许可证" />
<el-option label="其他附件" value="其他附件" />
</el-select>
</template>
</el-table-column>
<el-table-column label="文件名称" min-width="180">
<template #default="{ row }">
<vehicle-attachment-upload
v-model="row.files"
:readonly="readonly"
:multiple="false"
accept=".pdf,.bmp,.jpeg,.png,.jpg,.doc,.docx,.ppt,.pptx,.xlsx,.xls,.eml,.msg,.zip"
:max-size="500"
button-text="上传文件"
:show-tip="false"
@success="file => handleQualificationUploadSuccess(file, row)"
/>
</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">
<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>
<div class="change-record-section">
<div class="section-title dialog-section-title">变更记录</div>
<el-table :data="archiveForm.changeRecords" border>
<el-table-column label="序号" type="index" width="90" align="center" />
<el-table-column label="变更日期" prop="changeTime" min-width="170" />
<el-table-column label="变更内容" prop="changeContent" min-width="260" />
<el-table-column label="变更账号" prop="changeUserName" min-width="160" />
</el-table>
</div>
<template #footer>
<el-button @click="archiveBox = false">{{ readonly ? '关闭' : '取消' }}</el-button>
<el-button type="primary" v-if="!readonly" @click="saveArchive">保存</el-button>
<el-button type="success" v-if="!readonly" @click="saveAndSubmitArchive">提交</el-button>
<el-button v-if="!readonly" @click="archiveBox = false">返回</el-button>
</template>
</el-dialog>
<el-dialog
:title="contactDialogTitle"
append-to-body
v-model="contactBox"
width="680px"
class="contact-dialog"
@closed="resetContact"
>
<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" maxlength="200" />
</el-form-item>
</el-form>
<template #footer>
<div class="contact-dialog__footer-actions">
<el-button type="primary" @click="saveContact">保存</el-button>
<el-button @click="contactBox = false">返回</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"
>
<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-select v-model="receiptForm.bankName" filterable clearable>
<el-option
v-for="item in bankListOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</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" maxlength="200" />
</el-form-item>
</el-form>
<template #footer>
<div class="receipt-dialog__footer-actions">
<el-button type="primary" @click="saveReceipt">保存</el-button>
<el-button @click="receiptBox = false">返回</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"
>
<div class="invoice-form__section-title">发票信息</div>
<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="bankName">
<el-select v-model="invoiceForm.bankName" filterable clearable>
<el-option
v-for="item in bankListOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="银行账号" prop="bankAccount">
<el-input v-model="invoiceForm.bankAccount" maxlength="50" />
</el-form-item>
</el-col>
</el-row>
<div class="invoice-form__section-title">邮寄信息</div>
<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"
maxlength="100"
placeholder="地址控件选择"
/>
<el-input
v-model="invoiceForm.receiverDetailAddress"
maxlength="255"
placeholder="请输入详细地址"
/>
</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>
</el-form>
<template #footer>
<div class="invoice-dialog__footer-actions">
<el-button type="primary" @click="saveInvoice">保存</el-button>
<el-button @click="invoiceBox = false">返回</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"
>
<el-form
ref="scoreForm"
:model="currentScore"
label-position="right"
label-width="auto"
:disabled="readonly"
class="score-detail-form"
>
<el-row :gutter="42">
<el-col :span="12">
<el-form-item label="评定日期" required>
<el-date-picker
v-model="currentScore.scoreDate"
type="date"
value-format="YYYY-MM-DD"
/>
</el-form-item>
<el-form-item label="拟申请总资金使用额度(万元)" required>
<el-input
v-model="currentScore.applyCreditLimit"
maxlength="18"
@input="value => handleScoreDecimalInput('applyCreditLimit', value)"
/>
</el-form-item>
<el-form-item label="最终得分">
<el-input :model-value="formatScoreValue(currentScore.finalScore)" disabled />
</el-form-item>
<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="12">
<el-form-item label="最大资金使用额度(万元)">
<el-input
:model-value="formatScoreValue(currentScore.maxCreditLimit)"
disabled
/>
</el-form-item>
<el-form-item label="信用等级">
<el-input :model-value="formatScoreValue(currentScore.creditLevel)" disabled />
</el-form-item>
<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="24">
<el-form-item label="备注">
<el-input v-model="currentScore.remark" maxlength="200" />
</el-form-item>
</el-col>
<el-col :span="24">
<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="item.id"
/>
</el-select>
</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">
<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>
</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(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"
:disabled="readonly"
@input="value => handleScoreDetailScoreInput(detail, 'reviewScore', value)"
>
<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>自评:{{ row.selfScore }}分</span>
<span>复评:{{ 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>
<div class="score-material-section">
<div class="score-material-section__head">
<div class="dialog-section-title">证明材料</div>
<vehicle-attachment-upload
v-if="!readonly"
v-model="scoreAttachmentFiles"
button-text="上传附件"
button-icon="el-icon-upload"
:show-tip="false"
@success="handleScoreAttachmentUploadSuccess"
/>
</div>
<el-table :data="scoreAttachmentFiles" border class="score-material-table">
<el-table-column label="序号" type="index" width="90" align="center" />
<el-table-column label="文件名" prop="name" min-width="220" />
<el-table-column label="附件描述" prop="description" min-width="220" />
<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">
<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="primary" @click="deleteScoreAttachment($index)">
删除
</el-link>
</template>
</el-table-column>
</el-table>
</div>
<template #footer>
<el-button type="primary" v-if="!readonly" @click="saveScoreDetail">保存</el-button>
<el-button type="primary" v-if="!readonly" @click="confirmReviewScore">复评确认</el-button>
<el-button type="primary" @click="scoreBox = false">关闭</el-button>
</template>
</el-dialog>
</basic-container>
</template>
<script>
import {
getList,
getDetail,
submit,
submitApproval,
withdrawApproval,
approve,
reject,
changeStatus,
remove,
getScoreTemplate,
} 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 { downloadXls } from '@/utils/util';
import { getToken } from '@/utils/auth';
import { getUploadHeaders } from '@/utils/upload';
import { normalizeSearchRangeParams } from '@/utils/search-range';
import { ArrowRight } from '@element-plus/icons-vue';
import { mapGetters } from 'vuex';
import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
const createTimeRangeMap = {
createTimeRange: ['createTimeStart', 'createTimeEnd'],
};
const AMAP_KEY = '653b7cf105ad7fb8ec9b2f5198ade315';
const AMAP_SECURITY_CODE = '5aab65c632e48ebae0d0e28f5b28e21a';
let amapLoader;
export default {
components: {
ArrowRight,
},
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 validateBusinessEndDate = (rule, value, callback) => {
if (this.archiveForm.businessTermType === '固定期限' && !value) {
callback(new Error('请选择营业期限截止日'));
} else {
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,
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],
},
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: [],
deptTree: [],
deptOptions: [],
regionOptions: [],
customerNatureOptions: [],
customerTypeOptions: [],
businessScopeOptions: [],
bankListOptions: [],
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' },
{
pattern: /^[A-Z0-9]{18}$/,
message: '统一社会信用代码必须为18位大写字母或数字',
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' }],
businessEndDate: [{ validator: validateBusinessEndDate, trigger: 'change' }],
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' }],
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: 260,
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,
searchType: 'select',
searchOrder: 1,
searchValue: '',
minWidth: 180,
dicData: [{ label: '全部', value: '' }],
},
{
label: '准入标准',
prop: 'accessType',
slot: true,
type: 'select',
search: true,
searchOrder: 8,
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',
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();
},
computed: {
...mapGetters(['permission', 'userInfo']),
isAdmin() {
const authority = this.userInfo.authority || '';
return authority.includes('admin');
},
permissionList() {
return {
addBtn: false,
};
},
regionCascaderProps() {
return {
value: 'id',
label: 'title',
children: 'children',
emitPath: true,
};
},
ids() {
let ids = [];
this.selectionList.forEach(ele => {
ids.push(ele.id);
});
return ids.join(',');
},
dialogTitle() {
if (this.readonly) return '查看客商档案';
return this.archiveForm.id ? '编辑客商档案' : '新增客商档案';
},
contactDialogTitle() {
return this.contactIndex > -1 ? '编辑联系人' : '新增联系人';
},
receiptDialogTitle() {
return this.receiptIndex > -1 ? '编辑收款信息' : '新增收款信息';
},
invoiceDialogTitle() {
return this.invoiceIndex > -1 ? '编辑发票信息' : '新增发票信息';
},
missingQualificationText() {
if (this.archiveForm.accessType !== 'formal') return '';
const types = this.qualificationFiles
.filter(item => item.url)
.map(item => item.type || item.name)
.filter(Boolean);
const requiredMaterials = [
{ label: '法人身份证', aliases: ['法人身份证'] },
{ label: '道路运输许可证', aliases: ['道路运输许可证', '道路运输证'] },
];
const missing = requiredMaterials
.filter(
item => !types.some(type => item.aliases.some(alias => String(type).includes(alias)))
)
.map(item => item.label);
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.scoreList
.slice(start, start + this.scorePage.pageSize)
.map((item, index) => ({
...item,
__raw: item,
__rawIndex: start + index,
__index: start + index + 1,
maxCreditLimit: item.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: '减分项目' },
];
return 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)),
};
});
},
},
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);
},
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: '',
taxNo: '',
bankName: '',
registeredPhone: '',
bankAccount: '',
registeredRegionName: '',
registeredDetailAddress: '',
registeredAddress: '',
email: '',
receiverName: '',
receiverPhone: '',
receiverRegionName: '',
receiverDetailAddress: '',
receiverAddress: '',
isDefault: 0,
};
},
initDeptTree() {
getDeptTree(this.userInfo.tenantId)
.then(res => {
this.deptTree = this.normalizeDeptTree(res.data.data || []);
this.deptOptions = this.flattenDept(this.deptTree);
const deptColumn = this.findColumn(this.option.column, 'deptName');
deptColumn.dicData = [
{ label: '全部', value: '' },
...this.deptOptions.map(item => ({
label: item.label,
value: item.rawLabel,
})),
];
this.applyDefaultDeptSearch(deptColumn);
})
.finally(() => {
this.deptSearchInitialized = true;
this.$nextTick(() => this.onLoad(this.page, this.query));
});
},
applyDefaultDeptSearch(deptColumn) {
if (this.isAdmin) return;
const currentDeptId = this.userInfo.deptId || this.userInfo.dept_id;
const currentDept = this.deptOptions.find(
item => String(item.value) === String(currentDeptId)
);
const deptName = currentDept?.rawLabel || this.userInfo.deptName || this.userInfo.dept_name;
if (!deptName) return;
deptColumn.searchValue = deptName;
this.query = { ...this.query, deptName };
if (this.$refs.crud?.searchForm) {
this.$refs.crud.searchForm.deptName = deptName;
}
},
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('bank_list', 'bankListOptions');
},
initScoreQuantificationOptions() {
this.scoreQuantificationLoading = true;
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: item.id,
name: item.name || item.configName || item.title || item.id,
}));
})
.finally(() => {
this.scoreQuantificationLoading = false;
});
},
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 || []),
};
});
},
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;
},
onDeptChange(value) {
const deptIds = Array.isArray(value) ? value : value ? [value] : [];
const deptNames = deptIds
.map(id => this.deptOptions.find(item => String(item.value) === String(id))?.rawLabel)
.filter(Boolean);
this.archiveForm.deptIds = deptIds.join(',');
this.archiveForm.deptName = deptNames.join(',');
this.$refs.archiveForm?.validateField('deptName');
},
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) {
const value = detail.deptIds || detail.deptId;
return value
? String(value)
.split(',')
.map(item => item.trim())
.filter(item => item !== '')
: [];
},
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 = {}) {
return (
row.receiverAddress ||
[row.receiverRegionName, row.receiverDetailAddress]
.map(item => String(item || '').trim())
.filter(Boolean)
.join('')
);
},
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;
},
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;
}
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(),
name: String(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, row) {
row.name = file.name || row.name || '客商材料';
row.size = this.formatAttachmentSize(file.size || row.size);
row.uploadUserName = this.userInfo.realName || this.userInfo.userName || '';
row.uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss');
row.url = file.url || row.url || '';
row.files = [file];
},
downloadAttachments() {
const files = this.qualificationFiles.filter(item => item.url);
if (!files.length) {
this.$message.warning('暂无可下载附件');
return;
}
files.forEach(item => window.open(item.url, '_blank'));
},
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),
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,
tempApplyCreditLimit: this.normalizeOptionalAmount(score.tempApplyCreditLimit),
attachments: score.attachments || this.parseAttachments(score.proofAttachments),
details: score.details || [],
};
},
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;
},
getLatestCreditScore(scores = []) {
return (scores || [])
.map((score, index) => ({ score, index }))
.filter(item => String(item.score.creditLevel || '').trim())
.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;
})[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);
},
openArchive(row, readonly = false) {
this.readonly = readonly;
this.activeTab = 'scores';
this.resetDetailPagination();
if (row && row.id) {
getDetail(row.id).then(res => {
this.archiveForm = this.normalizeDetail(res.data.data);
this.qualificationFiles = this.parseAttachments(
this.archiveForm.qualificationAttachments
);
this.archiveBox = true;
});
return;
}
this.archiveForm = this.emptyArchive();
this.qualificationFiles = [];
this.archiveBox = true;
},
resetArchive() {
this.readonly = false;
this.archiveForm = this.emptyArchive();
this.qualificationFiles = [];
this.scoreBox = false;
this.resetScoreDialog();
this.resetDetailPagination();
this.contactBox = false;
this.contactIndex = -1;
this.contactForm = this.emptyContact();
this.receiptBox = false;
this.receiptIndex = -1;
this.receiptForm = this.emptyReceiptAccount();
this.invoiceBox = false;
this.invoiceIndex = -1;
this.invoiceForm = this.emptyInvoice();
this.$refs.archiveForm?.clearValidate();
},
validateFormalForApproval(archive) {
if (archive.accessType !== 'formal') return true;
if (!archive.qualificationAttachments) {
this.$message.warning('正式客商提交审核前必须维护客商材料');
return false;
}
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 ? deptIds[0] : '';
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 => this.calculateScore(score));
this.applyArchiveCreditFromScores(archive);
return archive;
},
saveArchive() {
this.$refs.archiveForm.validate(valid => {
if (!valid) {
this.$message.warning('请先完整填写必填项');
return;
}
submit(this.normalizeArchive()).then(() => {
this.archiveBox = false;
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.archiveBox = false;
this.onLoad(this.page);
this.$message({ type: 'success', message: '保存成功,请在列表提交审批' });
return;
}
submitApproval(id).then(() => {
this.archiveBox = false;
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;
if (!this.scoreQuantificationOptions.length) {
this.initScoreQuantificationOptions();
}
},
resetScoreDialog() {
this.scoreRecordIndex = -1;
this.currentScore = { details: [] };
this.scoreAttachmentFiles = [];
this.expandedScoreCategoryKeys = [];
},
handleScoreQuantificationChange(quantificationId) {
if (!quantificationId) {
this.currentScore.details = [];
this.expandedScoreCategoryKeys = [];
return;
}
Promise.all([
getScoreTemplate(quantificationId),
getCreditScoreQuantificationDetail(quantificationId),
]).then(([templateRes, quantificationRes]) => {
const templateData = templateRes.data.data || {};
const quantificationData = quantificationRes.data.data || {};
const template = this.normalizeScore({
...templateData,
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 || templateData.standards || [],
details: this.normalizeScoreTemplateDetails(templateData),
});
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,
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,
}))
),
}))
);
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 || '',
};
});
},
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 => ({
...item,
label: item.label || item.optionName || item.value,
value: item.value || item.optionName || item.label,
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 [];
}
},
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);
return;
}
}
detail[prop] = normalized;
this.calculateScore(this.currentScore);
},
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 : 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);
},
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 || [];
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 => !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(this.scorePageCount);
}
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 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;
},
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.isAdmin ? {} : { deptName: this.getCurrentDeptName() };
this.onLoad(this.page);
},
searchChange(params, done) {
this.query = this.buildQuery(params);
this.page.currentPage = 1;
this.onLoad(this.page);
done();
},
selectionChange(list) {
this.selectionList = list;
},
selectionClear() {
this.selectionList = [];
this.$refs.crud.toggleSelection();
},
currentChange(currentPage) {
this.page.currentPage = currentPage;
},
sizeChange(pageSize) {
this.page.pageSize = pageSize;
},
refreshChange() {
this.onLoad(this.page, this.query);
},
onLoad(page, params = {}) {
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);
},
getCurrentDeptName() {
const currentDeptId = this.userInfo.deptId || this.userInfo.dept_id;
const currentDept = this.deptOptions.find(
item => String(item.value) === String(currentDeptId)
);
return currentDept?.rawLabel || this.userInfo.deptName || this.userInfo.dept_name || '';
},
},
};
</script>
<style lang="scss" scoped>
.customer-archive-page {
:deep(.el-table th .cell),
:deep(.el-table td .cell) {
white-space: nowrap;
}
}
.archive-form {
:deep(.el-input-number),
:deep(.el-select),
:deep(.el-cascader),
:deep(.el-date-editor) {
width: 100%;
}
}
.archive-form__group {
padding: 16px 20px;
margin-bottom: 16px;
background-color: #f0f2f5;
border-radius: 8px;
.dialog-section-title {
margin: 0 0 12px;
font-size: 14px;
font-weight: 500;
color: #303133;
}
// 内层白色卡片:字段区域白底,浮于灰色分组块之上
> .el-row {
margin: 0;
padding: 18px 16px 4px;
background-color: #fff;
border-radius: 6px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
}
.business-term-field,
.archive-form__address {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
}
.business-term-field {
:deep(.el-date-editor) {
flex: 1;
min-width: 0;
}
:deep(.el-checkbox) {
flex: 0 0 auto;
margin-right: 0;
}
}
.archive-form__address {
.el-input,
:deep(.el-cascader) {
flex: 1;
min-width: 0;
}
:deep(.el-cascader) {
flex: 0 0 36%;
}
}
.section-title {
margin: 16px 0 12px;
font-size: 18px;
font-weight: 700;
color: #303133;
}
.section-head {
display: flex;
align-items: center;
justify-content: space-between;
margin: 18px 0 12px;
.section-title {
margin: 0;
}
}
.section-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: flex-end;
}
.archive-rule-alert {
margin-bottom: 12px;
}
.archive-tabs {
margin-top: 18px;
}
.score-list-table {
width: 100%;
}
.score-detail-form {
padding: 8px 36px 0;
:deep(.el-form-item) {
margin-bottom: 18px;
}
:deep(.el-form-item__label) {
color: #70757a;
font-size: 15px;
font-weight: 600;
}
:deep(.el-input),
:deep(.el-select),
:deep(.el-date-editor) {
width: 230px;
}
:deep(.el-col:nth-child(2) .el-form-item) {
justify-content: flex-end;
}
}
.score-detail-form__range {
display: flex;
align-items: center;
gap: 14px;
:deep(.el-date-editor) {
width: 230px;
}
}
.score-category-table {
width: calc(100% - 72px);
margin: 8px 36px 0;
: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%;
}
}
.customer-material-section {
margin-top: 18px;
}
.change-record-section {
margin-top: 18px;
}
.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: 16px;
}
:deep(.el-input) {
width: 100%;
}
}
.invoice-form {
max-width: none;
&__section-title {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
margin: 8px 0 20px;
font-size: 16px;
font-weight: 700;
color: #303133;
&::before,
&::after {
flex: 1;
height: 1px;
content: '';
background-color: #dcdfe6;
}
}
}
.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: center;
gap: 12px;
}
.receipt-form {
:deep(.el-input),
:deep(.el-select) {
width: 60%;
}
}
.receipt-dialog__footer-actions {
display: flex;
justify-content: center;
gap: 12px;
}
.invoice-dialog__footer-actions {
display: flex;
justify-content: center;
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%;
}
}
.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;
.el-button {
min-width: 160px;
height: 42px;
margin: 0 18px;
}
}
.score-material-section {
margin: 26px 36px 0;
}
.score-material-section__head {
display: flex;
align-items: center;
gap: 18px;
margin-bottom: 16px;
.dialog-section-title {
margin: 0;
}
:deep(.vehicle-attachment-upload) {
width: auto;
}
:deep(.el-upload-list) {
display: none;
}
}
.score-material-table {
width: 100%;
}
:deep(.archive-dialog .el-dialog__body) {
max-height: 72vh;
overflow: auto;
}
: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;
}
</style>