2525 lines
104 KiB
Vue
2525 lines
104 KiB
Vue
<template>
|
||
<div class="app-container">
|
||
<!-- 顶部:返回 + 标题 -->
|
||
<el-card shadow="never" class="head-card">
|
||
<div class="head-bar">
|
||
<el-button icon="ArrowLeft" @click="goBack">返回</el-button>
|
||
<div class="head-title">
|
||
<span class="customer-name">{{ customer.customerName || route.query.customerName || '-' }}</span>
|
||
<el-tag size="small" type="info">客户余额</el-tag>
|
||
</div>
|
||
<div class="head-code">客户编码:{{ customer.customerCode || route.query.customerCode || '-' }}</div>
|
||
<div class="head-right">
|
||
<el-tag type="success" size="small">本位币账户 CNY</el-tag>
|
||
<el-button
|
||
icon="Document"
|
||
@click="openBillDialog"
|
||
v-hasPermi="['customersupplier:customerinfo:bill']"
|
||
>查看账单</el-button>
|
||
<el-button
|
||
type="primary"
|
||
icon="Money"
|
||
@click="openAdjustDialog()"
|
||
v-hasPermi="['customer:credit:adjust']"
|
||
>额度调整申请</el-button>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
|
||
<customer-bill-list-dialog
|
||
v-if="billDialogVisible"
|
||
:key="currentCustomerId"
|
||
v-model="billDialogVisible"
|
||
:customer-id="currentCustomerId"
|
||
:customer-name="customer.customerName || route.query.customerName || ''"
|
||
/>
|
||
|
||
<!-- 余额概览(数据来源 customer_account) -->
|
||
<el-row :gutter="12" class="balance-row">
|
||
<el-col :xs="12" :sm="8" :md="4">
|
||
<el-card shadow="hover" class="balance-card">
|
||
<div class="balance-label">账户余额(CNY)</div>
|
||
<div class="balance-value primary">{{ formatMoney(currentAccount.balance) }}</div>
|
||
</el-card>
|
||
</el-col>
|
||
<!--
|
||
|
||
<el-col :xs="12" :sm="8" :md="4">
|
||
<el-card shadow="hover" class="balance-card">
|
||
<div class="balance-label">冻结余额</div>
|
||
<div class="balance-value warning">{{ formatMoney(currentAccount.frozenBalance) }}</div>
|
||
</el-card>
|
||
</el-col>
|
||
-->
|
||
<el-col :xs="12" :sm="8" :md="4">
|
||
<el-card shadow="hover" class="balance-card">
|
||
<div class="balance-label">
|
||
信用额度
|
||
<el-button link type="primary" size="small" @click="openAdjustDialog('CREDIT')"
|
||
v-hasPermi="['customer:credit:adjust']">调整</el-button>
|
||
</div>
|
||
<div class="balance-value">{{ formatMoney(currentAccount.totalCredit) }}</div>
|
||
<div class="balance-sub" :style="creditPeriodExpired ? 'color: var(--el-color-danger);' : ''">{{ creditPeriodText }}</div>
|
||
</el-card>
|
||
</el-col>
|
||
<el-col :xs="12" :sm="8" :md="4">
|
||
<el-card shadow="hover" class="balance-card">
|
||
<div class="balance-label">
|
||
额外额度
|
||
<el-button link type="primary" size="small" @click="openAdjustDialog('TEMP')"
|
||
v-hasPermi="['customer:credit:adjust']">调整</el-button>
|
||
</div>
|
||
<div class="balance-value">{{ formatMoney(currentAccount.extraCredit) }}</div>
|
||
<div class="balance-sub" :style="tempCreditPeriodExpired ? 'color: var(--el-color-danger);' : ''">{{ tempCreditPeriodText }}</div>
|
||
</el-card>
|
||
</el-col>
|
||
<el-col :xs="12" :sm="8" :md="4">
|
||
<el-card shadow="hover" class="balance-card">
|
||
<div class="balance-label">可用额度</div>
|
||
<div class="balance-value" :class="availableOverLimit ? 'danger' : 'success'">{{ availableCredit }}</div>
|
||
<div class="balance-sub" v-if="availableOverLimit" style="color: var(--el-color-danger);">
|
||
已超额,可继续计费,请及时催收回款
|
||
</div>
|
||
</el-card>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<!-- 明细区域 -->
|
||
<el-card shadow="never">
|
||
<el-tabs v-model="activeTab">
|
||
<!-- ====== 收款记录 ====== -->
|
||
<el-tab-pane label="收款记录" name="receipt">
|
||
<el-form :model="receiptQuery" :inline="true" class="search-form">
|
||
<el-form-item label="收款类型" prop="receiptType">
|
||
<el-select v-model="receiptQuery.receiptType" placeholder="全部" clearable style="width: 130px;">
|
||
<el-option v-for="opt in receiptTypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="状态" prop="status">
|
||
<el-select v-model="receiptQuery.status" placeholder="全部" clearable style="width: 120px;">
|
||
<el-option v-for="opt in receiptStatusOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="收款日期">
|
||
<el-date-picker
|
||
v-model="receiptDateRange"
|
||
value-format="YYYY-MM-DD"
|
||
type="daterange"
|
||
range-separator="-"
|
||
start-placeholder="开始日期"
|
||
end-placeholder="结束日期"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" icon="Search" @click="getReceiptList">搜索</el-button>
|
||
<el-button icon="Refresh" @click="resetReceiptQuery">重置</el-button>
|
||
<el-button type="success" icon="Plus" @click="handleAddReceipt"
|
||
v-hasPermi="['customersupplier:customerinfo:add']">登记收款</el-button>
|
||
<el-button type="warning" icon="Download" @click="handleExportReceipt"
|
||
v-hasPermi="['customersupplier:customerinfo:export']">导出</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
|
||
<el-table :data="receiptList" border v-loading="receiptLoading">
|
||
<el-table-column label="收款单号" align="center" prop="receiptNo" width="190" />
|
||
<el-table-column label="收款日期" align="center" prop="receivedAt" width="160">
|
||
<template #default="scope">{{ formatDateTime(scope.row.receivedAt) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="收款类型" align="center" prop="receiptType" width="100">
|
||
<template #default="scope">
|
||
<el-tag :type="receiptTypeTag(scope.row.receiptType)">{{ formatOption(receiptTypeOptions, scope.row.receiptType) }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="原币金额" align="center" width="130">
|
||
<template #default="scope">
|
||
<span>{{ scope.row.currency }} {{ formatMoney(scope.row.amount) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="汇率" align="center" width="100">
|
||
<template #default="scope">{{ formatRate(scope.row.exchangeRate) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="汇率来源" align="center" width="110">
|
||
<template #default="scope">
|
||
<el-tooltip v-if="scope.row.rateSourceDesc" :content="scope.row.rateSourceDesc" placement="top">
|
||
<el-tag :type="receiptRateSourceTag(scope.row.rateSource)" size="small">
|
||
{{ receiptRateSourceLabel(scope.row.rateSource) }}
|
||
</el-tag>
|
||
</el-tooltip>
|
||
<el-tag v-else :type="receiptRateSourceTag(scope.row.rateSource)" size="small">
|
||
{{ receiptRateSourceLabel(scope.row.rateSource) }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="本位币金额(CNY)" align="center" width="140">
|
||
<template #default="scope">
|
||
<span class="money-in">{{ formatMoney(scope.row.baseAmount) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<!-- <el-table-column label="待核销(CNY)" align="center" prop="unallocatedAmount" width="110" />-->
|
||
<el-table-column label="收款方式" align="center" prop="payMethod" width="110">
|
||
<template #default="scope">{{ formatOption(payMethodOptions, scope.row.payMethod) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="客户银行账号" align="center" width="200" show-overflow-tooltip>
|
||
<template #default="scope">
|
||
<span v-if="scope.row.bankAccountNo">
|
||
{{ scope.row.bankName ? scope.row.bankName + ' ' : '' }}{{ scope.row.bankAccountNo }}
|
||
</span>
|
||
<span v-else>-</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="银行/平台流水号" align="center" prop="bankTransactionNo" width="170" show-overflow-tooltip />
|
||
<el-table-column label="状态" align="center" prop="status" width="90">
|
||
<template #default="scope">
|
||
<el-tag :type="receiptStatusTag(scope.row.status)">{{ formatOption(receiptStatusOptions, scope.row.status) }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="附件" align="center" width="70">
|
||
<template #default="scope">
|
||
<el-badge v-if="scope.row.attachmentCount > 0" :value="scope.row.attachmentCount" type="primary">
|
||
<el-icon><Paperclip /></el-icon>
|
||
</el-badge>
|
||
<span v-else>-</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="登记人" align="center" prop="createBy" width="100" />
|
||
<el-table-column label="操作" align="center" width="200" fixed="right">
|
||
<template #default="scope">
|
||
<el-button v-if="scope.row.status === 'PENDING'" link type="success" size="small" @click="handleConfirmReceipt(scope.row)" v-hasPermi="['customer:balance:review']">审核通过</el-button>
|
||
<el-button v-if="scope.row.status === 'PENDING'" link type="warning" size="small" @click="handleCancelReceipt(scope.row)" v-hasPermi="['customersupplier:customerinfo:edit']">取消</el-button>
|
||
<el-button link type="primary" size="small" @click="handleViewReceipt(scope.row)" v-hasPermi="['customersupplier:customerinfo:query']">查看</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<pagination
|
||
v-show="receiptTotal > 0"
|
||
:total="receiptTotal"
|
||
v-model:page="receiptQuery.pageNum"
|
||
v-model:limit="receiptQuery.pageSize"
|
||
@pagination="getReceiptList"
|
||
/>
|
||
</el-tab-pane>
|
||
|
||
<!-- ====== 余额流水 ====== -->
|
||
<el-tab-pane label="余额流水" name="flow">
|
||
<el-form :model="flowQuery" :inline="true" class="search-form">
|
||
<el-form-item label="业务类型" prop="bizType">
|
||
<el-select v-model="flowQuery.bizType" placeholder="全部" clearable style="width: 130px;">
|
||
<el-option v-for="opt in bizTypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="收支方向" prop="direction">
|
||
<el-select v-model="flowQuery.direction" placeholder="全部" clearable style="width: 120px;">
|
||
<el-option label="收入" :value="1" />
|
||
<el-option label="支出" :value="-1" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="日期">
|
||
<el-date-picker
|
||
v-model="flowDateRange"
|
||
value-format="YYYY-MM-DD"
|
||
type="daterange"
|
||
range-separator="-"
|
||
start-placeholder="开始日期"
|
||
end-placeholder="结束日期"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" icon="Search" @click="getFlowList">搜索</el-button>
|
||
<el-button icon="Refresh" @click="resetFlowQuery">重置</el-button>
|
||
<el-button type="warning" icon="Download" @click="handleExportFlow"
|
||
v-hasPermi="['customersupplier:customerinfo:export']">导出</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
|
||
<el-table :data="flowList" border v-loading="flowLoading">
|
||
<el-table-column label="流水号" align="center" prop="flowNo" width="190" />
|
||
<el-table-column label="时间" align="center" prop="createTime" width="160">
|
||
<template #default="scope">{{ formatDateTime(scope.row.createTime) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="业务类型" align="center" prop="bizType" width="100">
|
||
<template #default="scope">
|
||
<el-tag :type="bizTypeTag(scope.row.bizType)">{{ formatOption(bizTypeOptions, scope.row.bizType) }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="收支" align="center" width="80">
|
||
<template #default="scope">
|
||
<el-tag v-if="scope.row.direction === 1" type="success">收入</el-tag>
|
||
<el-tag v-else type="danger">支出</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="原币金额" align="center" width="130">
|
||
<template #default="scope">
|
||
{{ scope.row.currency }} {{ formatMoney(scope.row.amount) }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="汇率" align="center" width="100">
|
||
<template #default="scope">{{ formatRate(scope.row.exchangeRate) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="本位币变动(CNY)" align="center" width="140">
|
||
<template #default="scope">
|
||
<span :class="scope.row.direction === 1 ? 'money-in' : 'money-out'">
|
||
{{ scope.row.direction === 1 ? '+' : '-' }}{{ formatMoney(scope.row.baseAmount != null ? scope.row.baseAmount : scope.row.amount) }}
|
||
</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="变动前余额" align="center" prop="beforeBalance" width="120" />
|
||
<el-table-column label="变动后余额" align="center" prop="afterBalance" width="120" />
|
||
<el-table-column label="关联单号" align="center" prop="bizNo" width="180" show-overflow-tooltip />
|
||
<el-table-column label="操作人" align="center" prop="createBy" width="100" />
|
||
<el-table-column label="备注" align="center" prop="remark" show-overflow-tooltip min-width="160" />
|
||
</el-table>
|
||
|
||
<pagination
|
||
v-show="flowTotal > 0"
|
||
:total="flowTotal"
|
||
v-model:page="flowQuery.pageNum"
|
||
v-model:limit="flowQuery.pageSize"
|
||
@pagination="getFlowList"
|
||
/>
|
||
</el-tab-pane>
|
||
|
||
<!-- ====== 额度流水 ====== -->
|
||
<el-tab-pane label="额度流水" name="creditFlow">
|
||
<el-form :model="creditFlowQuery" :inline="true" class="search-form">
|
||
<el-form-item label="额度类型" prop="creditType">
|
||
<el-select v-model="creditFlowQuery.creditType" placeholder="全部" clearable style="width: 130px;">
|
||
<el-option v-for="opt in creditTypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="变动类型" prop="changeType">
|
||
<el-select v-model="creditFlowQuery.changeType" placeholder="全部" clearable style="width: 130px;">
|
||
<el-option v-for="opt in creditChangeTypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="日期">
|
||
<el-date-picker
|
||
v-model="creditFlowDateRange"
|
||
value-format="YYYY-MM-DD"
|
||
type="daterange"
|
||
range-separator="-"
|
||
start-placeholder="开始日期"
|
||
end-placeholder="结束日期"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" icon="Search" @click="getCreditFlowList">搜索</el-button>
|
||
<el-button icon="Refresh" @click="resetCreditFlowQuery">重置</el-button>
|
||
<el-button type="warning" icon="Download" @click="handleExportCreditFlow"
|
||
v-hasPermi="['customersupplier:customerinfo:export']">导出</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
|
||
<el-table :data="creditFlowList" border v-loading="creditFlowLoading">
|
||
<el-table-column label="流水号" align="center" prop="flowNo" width="190" />
|
||
<el-table-column label="时间" align="center" prop="createTime" width="160">
|
||
<template #default="scope">{{ formatDateTime(scope.row.createTime) }}</template>
|
||
</el-table-column>
|
||
<!-- <el-table-column label="额度类型" align="center" prop="creditType" width="100">-->
|
||
<!-- <template #default="scope">-->
|
||
<!-- <el-tag :type="scope.row.creditType === 'TEMP' ? 'warning' : ''">-->
|
||
<!-- {{ formatOption(creditTypeOptions, scope.row.creditType) }}-->
|
||
<!-- </el-tag>-->
|
||
<!-- </template>-->
|
||
<!-- </el-table-column>-->
|
||
<el-table-column label="业务单号" align="center" prop="bizNo" width="190" />
|
||
<el-table-column label="变动类型" align="center" prop="changeType" width="100">
|
||
<template #default="scope">
|
||
<el-tag :type="creditChangeTag(scope.row.changeType)">
|
||
{{ formatOption(creditChangeTypeOptions, scope.row.changeType) }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="变动维度" align="center" width="130">
|
||
<template #default="scope">
|
||
<el-tag v-if="isCreditUsageFlow(scope.row.changeType)" type="warning">授信占用(已用额度)</el-tag>
|
||
<el-tag v-else type="primary">授信额度</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="调整方式" align="center" prop="adjustMode" width="90">
|
||
<template #default="scope">{{ formatOption(adjustModeOptions, scope.row.adjustMode) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="方向" align="center" width="80">
|
||
<template #default="scope">
|
||
<el-tag v-if="scope.row.direction === 1" type="success">增加</el-tag>
|
||
<el-tag v-else-if="scope.row.direction === -1" type="danger">减少</el-tag>
|
||
<span v-else>-</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="变动前" align="center" prop="beforeAmount" width="100">
|
||
<template #default="scope">{{ formatMoney(scope.row.beforeAmount) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="变动额" align="center" width="100">
|
||
<template #default="scope">
|
||
<span :class="scope.row.direction === 1 ? 'money-in' : 'money-out'">
|
||
{{ scope.row.direction === 1 ? '+' : '-' }}{{ formatMoney(scope.row.changeAmount) }}
|
||
</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="变动后" align="center" prop="afterAmount" width="100">
|
||
<template #default="scope">{{ formatMoney(scope.row.afterAmount) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="已用额度快照" align="center" prop="usedCredit" width="110">
|
||
<template #default="scope">
|
||
<span v-if="isCreditUsageFlow(scope.row.changeType)">-</span>
|
||
<span v-else>{{ formatMoney(scope.row.usedCredit) }}</span>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="生效日期" align="center" prop="effectiveDate" width="110">
|
||
<template #default="scope">{{ scope.row.effectiveDate || '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="失效日期" align="center" prop="expireDate" width="110">
|
||
<template #default="scope">{{ scope.row.expireDate || '长期' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="调整原因" align="center" prop="reason" show-overflow-tooltip min-width="160" />
|
||
<el-table-column label="操作人" align="center" prop="createBy" width="100" />
|
||
<el-table-column label="备注" align="center" prop="remark" show-overflow-tooltip min-width="160" />
|
||
</el-table>
|
||
|
||
<pagination
|
||
v-show="creditFlowTotal > 0"
|
||
:total="creditFlowTotal"
|
||
v-model:page="creditFlowQuery.pageNum"
|
||
v-model:limit="creditFlowQuery.pageSize"
|
||
@pagination="getCreditFlowList"
|
||
/>
|
||
</el-tab-pane>
|
||
|
||
<!-- ====== 额度审批(信用/额外额度调整申请) ====== -->
|
||
<el-tab-pane label="额度审批" name="creditApply">
|
||
<el-alert type="warning" :closable="false" class="credit-apply-tip">
|
||
信用额度 / 额外额度调整需先提交申请,审批通过后额度才会调整并写入额度流水。
|
||
</el-alert>
|
||
<el-form :model="applyQuery" :inline="true" class="search-form">
|
||
<el-form-item label="额度类型" prop="creditType">
|
||
<el-select v-model="applyQuery.creditType" placeholder="全部" clearable style="width: 130px;">
|
||
<el-option v-for="opt in creditTypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="状态" prop="status">
|
||
<el-select v-model="applyQuery.status" placeholder="全部" clearable style="width: 130px;">
|
||
<el-option v-for="opt in applyStatusOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" icon="Search" @click="getApplyList">搜索</el-button>
|
||
<el-button icon="Refresh" @click="resetApplyQuery">重置</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
|
||
<el-table :data="applyList" border v-loading="applyLoading">
|
||
<el-table-column label="申请单号" align="center" prop="applyNo" width="180" />
|
||
<el-table-column label="额度类型" align="center" prop="creditType" width="100">
|
||
<template #default="scope">
|
||
<el-tag :type="scope.row.creditType === 'TEMP' ? 'warning' : ''">
|
||
{{ formatOption(creditTypeOptions, scope.row.creditType) }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="调整方式" align="center" prop="adjustMode" width="90">
|
||
<template #default="scope">{{ formatOption(adjustModeOptions, scope.row.adjustMode) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="调整金额" align="center" prop="changeAmount" width="110">
|
||
<template #default="scope">{{ formatMoney(scope.row.changeAmount) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="生效日期" align="center" prop="effectiveDate" width="100">
|
||
<template #default="scope">{{ scope.row.effectiveDate || '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="失效日期" align="center" prop="expireDate" width="100">
|
||
<template #default="scope">{{ scope.row.expireDate || '长期' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="调整原因" align="center" prop="reason" min-width="150" show-overflow-tooltip />
|
||
<el-table-column label="状态" align="center" prop="status" width="90">
|
||
<template #default="scope">
|
||
<el-tag :type="applyStatusTag(scope.row.status)" size="small">
|
||
{{ formatOption(applyStatusOptions, scope.row.status) }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="申请人" align="center" prop="applyBy" width="100" />
|
||
<el-table-column label="申请时间" align="center" prop="applyTime" width="150">
|
||
<template #default="scope">{{ formatDateTime(scope.row.applyTime) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="审批人" align="center" prop="approveBy" width="100">
|
||
<template #default="scope">{{ scope.row.approveBy || '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="额度池角色" align="center" prop="approveRoleName" width="110">
|
||
<template #default="scope">{{ scope.row.approveRoleName || '超管' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="审批时间" align="center" prop="approveTime" width="150">
|
||
<template #default="scope">{{ scope.row.approveTime ? formatDateTime(scope.row.approveTime) : '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="审批意见" align="center" prop="approveRemark" min-width="140" show-overflow-tooltip>
|
||
<template #default="scope">{{ scope.row.approveRemark || '-' }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" align="center" width="150" fixed="right">
|
||
<template #default="scope">
|
||
<template v-if="scope.row.status === 'PENDING'">
|
||
<el-button link type="success" size="small"
|
||
@click="handleApproveApply(scope.row)"
|
||
v-hasPermi="['customer:credit:approve']">通过</el-button>
|
||
<el-button link type="danger" size="small"
|
||
@click="handleRejectApply(scope.row)"
|
||
v-hasPermi="['customer:credit:approve']">驳回</el-button>
|
||
<el-button link type="warning" size="small"
|
||
@click="handleCancelApply(scope.row)"
|
||
v-hasPermi="['customer:credit:adjust']">撤销</el-button>
|
||
</template>
|
||
<span v-else>-</span>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
|
||
<pagination
|
||
v-show="applyTotal > 0"
|
||
:total="applyTotal"
|
||
v-model:page="applyQuery.pageNum"
|
||
v-model:limit="applyQuery.pageSize"
|
||
@pagination="getApplyList"
|
||
/>
|
||
</el-tab-pane>
|
||
<!-- ====== 账单核销(用账户余额核销应收账单) ====== -->
|
||
<el-tab-pane label="账单核销" name="billWriteoff">
|
||
<el-alert type="info" :closable="false" class="writeoff-summary">
|
||
<div class="wo-summary">
|
||
<span>待核销账单:<b>{{ unsettledList.length }}</b> 张</span>
|
||
<span>未核销总额:<b class="money-out">CNY {{ unsettledTotal }}</b></span>
|
||
<span>账户余额:<b class="money-in">CNY {{ formatMoney(currentAccount.balance) }}</b></span>
|
||
<el-button type="primary" link size="small" icon="Refresh" @click="loadBillWriteoffData">刷新</el-button>
|
||
</div>
|
||
</el-alert>
|
||
|
||
<div class="wo-section-title">待核销账单(已确认 / 部分核销)</div>
|
||
<el-table :data="unsettledList" border size="small" v-loading="unsettledLoading">
|
||
<el-table-column label="账单号" align="center" prop="billNo" width="170" show-overflow-tooltip />
|
||
<el-table-column label="账单名称" align="center" prop="billName" min-width="150" show-overflow-tooltip />
|
||
<el-table-column label="账期开始" align="center" prop="billPeriodStart" width="110">
|
||
<template #default="scope">{{ formatDate(scope.row.billPeriodStart) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="账期结束" align="center" prop="billPeriodEnd" width="110">
|
||
<template #default="scope">{{ formatDate(scope.row.billPeriodEnd) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="账单日期" align="center" prop="billDate" width="110">
|
||
<template #default="scope">{{ formatDate(scope.row.billDate) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="应收金额" align="center" width="120">
|
||
<template #default="scope">{{ scope.row.currency }} {{ formatMoney(scope.row.totalAmount) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="已核销" align="center" width="110">
|
||
<template #default="scope"><span class="money-in">{{ formatMoney(scope.row.writtenOffAmount) }}</span></template>
|
||
</el-table-column>
|
||
<el-table-column label="未核销" align="center" width="110">
|
||
<template #default="scope"><span class="money-out">{{ formatMoney(scope.row.unwrittenAmount) }}</span></template>
|
||
</el-table-column>
|
||
<el-table-column label="状态" align="center" width="100">
|
||
<template #default="scope">
|
||
<el-tag :type="billStatusTag(scope.row.status)" size="small">{{ billStatusLabel(scope.row.status) }}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" align="center" width="90" fixed="right">
|
||
<template #default="scope">
|
||
<el-button link type="primary" size="small" @click="openWriteoffDialog(scope.row)"
|
||
v-hasPermi="['customer:bill:writeoff']">核销</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
<template #empty>
|
||
<el-empty description="暂无待核销账单" :image-size="70" />
|
||
</template>
|
||
</el-table>
|
||
|
||
<div class="wo-section-title">余额核销记录</div>
|
||
<el-form :inline="true" class="search-form">
|
||
<el-form-item label="状态">
|
||
<el-select v-model="woQuery.status" placeholder="全部" clearable style="width: 130px;">
|
||
<el-option v-for="item in AR_BALANCE_WRITEOFF_STATUS_OPTIONS" :key="item.value" :label="item.label" :value="item.value" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" icon="Search" @click="getWriteoffList">搜索</el-button>
|
||
<el-button icon="Refresh" @click="resetWriteoffQuery">重置</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
<el-table :data="writeoffList" border size="small" v-loading="writeoffLoading">
|
||
<el-table-column label="核销单号" align="center" width="190">
|
||
<template #default="scope">
|
||
<div>{{ scope.row.writeoffNo }}</div>
|
||
<el-tag v-if="scope.row.receiptId" type="primary" size="small" effect="plain" style="margin-top: 2px;">收款联动</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="账单号" align="center" prop="billNo" width="170" show-overflow-tooltip />
|
||
<el-table-column label="账单名称" align="center" prop="billName" min-width="140" show-overflow-tooltip />
|
||
<el-table-column label="核销金额" align="center" width="120">
|
||
<template #default="scope"><span class="money-out">-{{ formatMoney(scope.row.writeoffAmount) }} {{ scope.row.currency }}</span></template>
|
||
</el-table-column>
|
||
<el-table-column label="核销日期" align="center" prop="writeoffDate" width="110">
|
||
<template #default="scope">{{ formatDate(scope.row.writeoffDate) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="状态" align="center" width="90">
|
||
<template #default="scope">
|
||
<el-tag :type="AR_BALANCE_WRITEOFF_STATUS_TYPES[scope.row.status] || 'info'" size="small">
|
||
{{ formatOption(AR_BALANCE_WRITEOFF_STATUS_OPTIONS, scope.row.status) }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="备注" align="center" prop="remark" min-width="130" show-overflow-tooltip />
|
||
<el-table-column label="登记人" align="center" prop="createBy" width="90" />
|
||
<el-table-column label="登记时间" align="center" prop="createTime" width="140">
|
||
<template #default="scope">{{ formatDateTime(scope.row.createTime) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="反核销原因" align="center" prop="cancelRemark" min-width="130" show-overflow-tooltip />
|
||
<el-table-column label="操作" align="center" width="90" fixed="right">
|
||
<template #default="scope">
|
||
<el-button v-if="scope.row.status === AR_BALANCE_WRITEOFF_STATUS.EFFECTIVE" link type="warning" size="small"
|
||
@click="openCancelWriteoffDialog(scope.row)"
|
||
v-hasPermi="['customer:bill:writeoff:cancel']">反核销</el-button>
|
||
<span v-else>-</span>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<pagination
|
||
v-show="writeoffTotal > 0"
|
||
:total="writeoffTotal"
|
||
v-model:page="woQuery.pageNum"
|
||
v-model:limit="woQuery.pageSize"
|
||
@pagination="getWriteoffList"
|
||
/>
|
||
</el-tab-pane>
|
||
</el-tabs>
|
||
</el-card>
|
||
|
||
<!-- ====== 登记收款弹窗 ====== -->
|
||
<el-dialog title="登记收款" v-model="receiptDialog.visible" width="780px" append-to-body @closed="onReceiptDialogClosed">
|
||
<el-form ref="receiptFormRef" :model="receiptDialog.form" :rules="receiptRules" label-width="110px">
|
||
<el-row>
|
||
<el-col :span="12">
|
||
<el-form-item label="收款日期" prop="receivedAt">
|
||
<el-date-picker
|
||
v-model="receiptDialog.form.receivedAt"
|
||
type="datetime"
|
||
value-format="YYYY-MM-DD HH:mm:ss"
|
||
placeholder="选择收款日期时间"
|
||
style="width: 100%;"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="收款金额" prop="amount">
|
||
<el-input v-model="receiptDialog.form.amount" placeholder="请输入原币金额" type="number">
|
||
<template #prepend>{{ receiptDialog.form.currency || 'CNY' }}</template>
|
||
</el-input>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="收款类型" prop="receiptType">
|
||
<el-select v-model="receiptDialog.form.receiptType" placeholder="请选择" style="width: 100%;">
|
||
<el-option v-for="opt in receiptTypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="币种" prop="currency">
|
||
<el-select v-model="receiptDialog.form.currency" placeholder="请选择" style="width: 100%;" @change="handleCurrencyChange">
|
||
<el-option v-for="c in currencyOptions" :key="c.value" :label="c.label" :value="c.value" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="汇率" prop="exchangeRate">
|
||
<el-input-number
|
||
v-model="receiptDialog.form.exchangeRate"
|
||
:precision="8"
|
||
:step="0.0001"
|
||
:min="0"
|
||
:disabled="receiptDialog.form.currency === 'CNY'"
|
||
controls-position="right"
|
||
style="width: 100%;"
|
||
@input="handleRateInput"
|
||
/>
|
||
<div class="rate-tip">
|
||
1 {{ receiptDialog.form.currency || 'CNY' }} = {{ formatRate(receiptDialog.form.exchangeRate) }} CNY
|
||
<el-tag v-if="receiptDialog.form.currency === 'CNY'" size="small" type="info" effect="plain" style="margin-left: 6px;">系统默认</el-tag>
|
||
<el-tag v-else-if="rateEdited" size="small" type="warning" effect="dark" style="margin-left: 6px;">手工修改</el-tag>
|
||
<el-tag v-else-if="receiptDialog.form.exchangeRate" size="small" type="success" effect="plain" style="margin-left: 6px;">汇率表{{ rateSourceInfo.source ? '·' + rateSourceInfo.source : '' }}</el-tag>
|
||
<span v-else style="color: var(--el-color-warning);"> 汇率表未配置,请手工录入</span>
|
||
<span v-if="receiptDialog.form.currency !== 'CNY'">,默认取汇率表,可手工修改</span>
|
||
</div>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="本位币金额">
|
||
<el-input :model-value="'CNY ' + baseAmountPreview" disabled>
|
||
<template #prepend>≈</template>
|
||
</el-input>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="收款方式" prop="payMethod">
|
||
<el-select v-model="receiptDialog.form.payMethod" placeholder="请选择" style="width: 100%;" clearable>
|
||
<el-option v-for="opt in payMethodOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="客户银行账号" prop="bankAccountNo">
|
||
<el-input
|
||
v-model="receiptDialog.form.bankAccountNo"
|
||
placeholder="客户付款银行账号(选填)"
|
||
maxlength="64"
|
||
clearable
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="流水号" prop="bankTransactionNo">
|
||
<el-input v-model="receiptDialog.form.bankTransactionNo" placeholder="银行/支付平台流水号" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="24">
|
||
<el-form-item label="备注" prop="remark">
|
||
<el-input v-model="receiptDialog.form.remark" type="textarea" :rows="2" placeholder="请输入备注" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="24">
|
||
<el-form-item label="核销账单">
|
||
<div class="receipt-wo-toolbar">
|
||
<el-button type="primary" plain icon="Plus" @click="openBillPicker">选择账单核销</el-button>
|
||
<span class="receipt-wo-summary">
|
||
本次核销合计:<b class="money-out">{{ formatMoney(receiptWriteoffTotal) }} CNY</b>
|
||
,收款折算本位币:<b>{{ formatMoney(receiptBaseCny) }} CNY</b>
|
||
<!-- ,核销后剩余入账:<b :class="receiptWriteoffRemain < 0 ? 'money-out' : 'money-in'">-->
|
||
<!-- {{ formatMoney(receiptWriteoffRemain) }} CNY-->
|
||
<!-- </b>-->
|
||
</span>
|
||
</div>
|
||
<el-table :data="receiptDialog.form.writeoffPlans" border size="small" max-height="220">
|
||
<el-table-column label="账单号" prop="billNo" align="center" width="170" show-overflow-tooltip />
|
||
<el-table-column label="账单名称" prop="billName" align="center" min-width="150" show-overflow-tooltip />
|
||
<el-table-column label="未核销金额" align="center" width="120">
|
||
<template #default="scope">{{ formatMoney(scope.row.unwrittenAmount) }} {{ scope.row.currency }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="本次核销金额(CNY)" align="center" width="160">
|
||
<template #default="scope">
|
||
<el-input-number
|
||
v-model="scope.row.writeoffAmount"
|
||
:min="0"
|
||
:max="Number(scope.row.unwrittenAmount)"
|
||
:precision="2"
|
||
:step="1"
|
||
:controls="false"
|
||
size="small"
|
||
style="width: 130px;"
|
||
/>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" align="center" width="70">
|
||
<template #default="scope">
|
||
<el-button link type="danger" icon="Delete" @click="removeReceiptBill(scope.$index)" />
|
||
</template>
|
||
</el-table-column>
|
||
<template #empty>
|
||
<span style="color: var(--el-text-color-secondary);">不选账单则整笔计入客户余额,可在“账单核销”页手工核销</span>
|
||
</template>
|
||
</el-table>
|
||
<div class="upload-tip">
|
||
当前仅支持<strong>人民币(CNY)账单</strong>核销,账单行金额均按 CNY 填写;
|
||
所选账单在收款单<strong>审核通过</strong>后一并核销,核销合计不能超过收款金额按汇率折算的本位币金额
|
||
({{ formatMoney(receiptBaseCny) }} CNY);<strong>核销不扣减客户余额</strong>,正常计入客户余额。
|
||
</div>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="24">
|
||
<el-form-item label="收款凭证">
|
||
<el-upload
|
||
drag
|
||
class="receipt-upload"
|
||
:show-file-list="false"
|
||
:action="uploadFileUrl"
|
||
:headers="uploadHeaders"
|
||
:data="{ module: 'receipt' }"
|
||
:before-upload="handleBeforeUpload"
|
||
:on-success="handleUploadSuccess"
|
||
:on-error="handleUploadError"
|
||
multiple
|
||
>
|
||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||
<div class="el-upload__text">将文件拖到此处,或<em>点击选择</em>(支持一次拖入多个文件)</div>
|
||
</el-upload>
|
||
<el-table :data="receiptDialog.form.attachmentList" border size="small" max-height="220">
|
||
<el-table-column label="序号" type="index" width="55" align="center" />
|
||
<el-table-column label="文件名称" prop="fileName" align="center" show-overflow-tooltip min-width="180" />
|
||
<el-table-column label="大小" align="center" width="100">
|
||
<template #default="scope">{{ formatFileSize(scope.row.fileSize) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="类型" prop="fileExtension" align="center" width="80" />
|
||
<el-table-column label="操作" align="center" width="80">
|
||
<template #default="scope">
|
||
<el-button link type="danger" icon="Delete" @click="handleRemoveUpload(scope.$index)" />
|
||
<el-button link type="primary" icon="Download" @click="handleDownload(scope.row)">下载</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div class="upload-tip">收款水单单独归档,不会显示在客户档案的附件资料中;支持 PDF/图片/Office/压缩包,单个不超过 50MB</div>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button type="primary" :loading="submitLoading" @click="submitReceipt">确 定</el-button>
|
||
<el-button @click="receiptDialog.visible = false">取 消</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- ====== 收款单查看弹窗 ====== -->
|
||
<el-dialog title="收款单详情" v-model="viewDialog.visible" width="720px" append-to-body>
|
||
<el-descriptions :column="2" border v-if="viewDialog.data" size="small">
|
||
<el-descriptions-item label="收款单号">{{ viewDialog.data.receiptNo }}</el-descriptions-item>
|
||
<el-descriptions-item label="状态">
|
||
<el-tag :type="receiptStatusTag(viewDialog.data.status)">{{ formatOption(receiptStatusOptions, viewDialog.data.status) }}</el-tag>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="收款日期">{{ formatDateTime(viewDialog.data.receivedAt) }}</el-descriptions-item>
|
||
<el-descriptions-item label="收款类型">
|
||
<el-tag :type="receiptTypeTag(viewDialog.data.receiptType)">{{ formatOption(receiptTypeOptions, viewDialog.data.receiptType) }}</el-tag>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="原币金额">
|
||
<span class="money-in">{{ viewDialog.data.currency }} {{ formatMoney(viewDialog.data.amount) }}</span>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="对CNY汇率">{{ formatRate(viewDialog.data.exchangeRate) }}</el-descriptions-item>
|
||
<el-descriptions-item label="汇率来源">
|
||
<el-tag :type="receiptRateSourceTag(viewDialog.data.rateSource)" size="small">
|
||
{{ receiptRateSourceLabel(viewDialog.data.rateSource) }}
|
||
</el-tag>
|
||
<span v-if="viewDialog.data.rateSourceDesc" style="margin-left: 8px; color: var(--el-text-color-secondary);">
|
||
{{ viewDialog.data.rateSourceDesc }}
|
||
</span>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="本位币金额">
|
||
<span class="money-in">CNY {{ formatMoney(viewDialog.data.baseAmount) }}</span>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="待核销金额">CNY {{ formatMoney(viewDialog.data.unallocatedAmount) }}</el-descriptions-item>
|
||
<el-descriptions-item label="收款方式">{{ formatOption(payMethodOptions, viewDialog.data.payMethod) }}</el-descriptions-item>
|
||
<el-descriptions-item label="客户银行账号">
|
||
<span v-if="viewDialog.data.bankAccountNo">
|
||
{{ viewDialog.data.bankName ? viewDialog.data.bankName + ' ' : '' }}{{ viewDialog.data.bankAccountNo }}
|
||
<span v-if="viewDialog.data.bankAccountName">({{ viewDialog.data.bankAccountName }})</span>
|
||
</span>
|
||
<span v-else>-</span>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="银行流水号">{{ viewDialog.data.bankTransactionNo || '-' }}</el-descriptions-item>
|
||
<el-descriptions-item label="确认时间">{{ formatDateTime(viewDialog.data.confirmedAt) }}</el-descriptions-item>
|
||
<el-descriptions-item label="登记人">{{ viewDialog.data.createBy || '-' }}</el-descriptions-item>
|
||
<el-descriptions-item label="备注" :span="2">{{ viewDialog.data.remark || '-' }}</el-descriptions-item>
|
||
</el-descriptions>
|
||
|
||
<template v-if="viewDialog.data && viewDialog.data.writeoffPlans && viewDialog.data.writeoffPlans.length">
|
||
<div class="attach-title">关联核销账单({{ viewDialog.data.writeoffPlans.length }})</div>
|
||
<el-table :data="viewDialog.data.writeoffPlans" border size="small" max-height="220" style="margin-bottom: 12px;">
|
||
<el-table-column label="账单号" prop="billNo" align="center" width="170" show-overflow-tooltip />
|
||
<el-table-column label="账单名称" prop="billName" align="center" min-width="150" show-overflow-tooltip />
|
||
<el-table-column label="本次核销金额" align="center" width="140">
|
||
<template #default="scope">{{ formatMoney(scope.row.writeoffAmount) }} {{ scope.row.currency }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="状态" align="center" width="100">
|
||
<template #default="scope">
|
||
<el-tag :type="receiptPlanStatusTag(scope.row.status)" size="small">
|
||
{{ receiptPlanStatusLabel(scope.row.status) }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="核销日期" align="center" width="110">
|
||
<template #default="scope">{{ scope.row.writeoffDate ? formatDate(scope.row.writeoffDate) : '-' }}</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</template>
|
||
|
||
<div class="attach-title">收款凭证附件</div>
|
||
<el-table :data="viewDialog.attachments" border size="small" max-height="240" v-loading="viewDialog.loading">
|
||
<el-table-column label="序号" type="index" width="55" align="center" />
|
||
<el-table-column label="文件名称" prop="fileName" align="center" show-overflow-tooltip min-width="200">
|
||
<template #default="scope">
|
||
<el-button link type="primary" @click="handleDownload(scope.row)">{{ scope.row.fileName }}</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="大小" align="center" width="100">
|
||
<template #default="scope">{{ formatFileSize(scope.row.fileSize) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="类型" prop="fileExtension" align="center" width="80" />
|
||
<el-table-column label="上传人" align="center" prop="createBy" width="100" />
|
||
<el-table-column label="操作" align="center" width="100">
|
||
<template #default="scope">
|
||
<el-button link type="primary" icon="Download" @click="handleDownload(scope.row)">下载</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
<template #empty>
|
||
<el-empty description="暂无收款凭证附件" :image-size="70" />
|
||
</template>
|
||
</el-table>
|
||
|
||
<template #footer>
|
||
<el-button
|
||
v-if="viewDialog.data && viewDialog.data.status === 'PENDING'"
|
||
type="success"
|
||
@click="handleConfirmFromView"
|
||
v-hasPermi="['customer:balance:review']"
|
||
>审核通过</el-button>
|
||
<el-button @click="viewDialog.visible = false">关 闭</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- ====== 额度调整申请弹窗 ====== -->
|
||
<el-dialog
|
||
:title="(creditDialog.form.creditType === 'TEMP' ? '额外额度调整申请' : '信用额度调整申请') + '(提交后需审批)'"
|
||
v-model="creditDialog.visible"
|
||
width="600px"
|
||
append-to-body
|
||
@closed="resetCreditForm"
|
||
>
|
||
<el-alert type="info" :closable="false" class="credit-tip">
|
||
<div>当前{{ creditDialog.form.creditType === 'TEMP' ? '临时额度' : '信用额度' }}:
|
||
<b>{{ formatMoney(creditBefore) }}</b>,账户余额:<b>{{ formatMoney(currentAccount.balance) }}</b>,可用额度:<b>{{ availableCredit }}</b></div>
|
||
</el-alert>
|
||
<el-form ref="creditFormRef" :model="creditDialog.form" :rules="creditRules" label-width="100px">
|
||
<el-row>
|
||
<el-col :span="12">
|
||
<el-form-item label="额度类型" prop="creditType">
|
||
<el-radio-group v-model="creditDialog.form.creditType" @change="onCreditTypeChange">
|
||
<el-radio value="CREDIT">信用额度</el-radio>
|
||
<el-radio value="TEMP">临时额度</el-radio>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="调整方式" prop="adjustMode">
|
||
<el-radio-group v-model="creditDialog.form.adjustMode">
|
||
<el-radio value="SET">设置为</el-radio>
|
||
<el-radio value="ADD">增加</el-radio>
|
||
<el-radio value="SUB">减少</el-radio>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="调整金额" prop="changeAmount">
|
||
<el-input-number
|
||
v-model="creditDialog.form.changeAmount"
|
||
:precision="2"
|
||
:step="1000"
|
||
:min="0.01"
|
||
controls-position="right"
|
||
style="width: 100%;"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="生效日期" prop="effectiveDate">
|
||
<el-date-picker
|
||
v-model="creditDialog.form.effectiveDate"
|
||
type="date"
|
||
value-format="YYYY-MM-DD"
|
||
placeholder="默认今天"
|
||
:disabled-date="afterToday"
|
||
style="width: 100%;"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="失效日期" prop="expireDate">
|
||
<el-date-picker
|
||
v-model="creditDialog.form.expireDate"
|
||
type="date"
|
||
value-format="YYYY-MM-DD"
|
||
placeholder="必选,到期自动清零"
|
||
style="width: 100%;"
|
||
/>
|
||
<div class="rate-tip">
|
||
{{ creditDialog.form.creditType === 'TEMP'
|
||
? '临时额度失效日期必填;失效日当天仍有效,次日访问账户时自动清零并记到期流水'
|
||
: '信用额度失效日期必填;失效日当天仍有效,次日自动清零;额度失效后可用额度相应减少,若账户余额为负将形成欠费(可继续计费,请及时催收回款)' }}
|
||
</div>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="24">
|
||
<el-form-item label="调整原因" prop="reason">
|
||
<el-input
|
||
v-model="creditDialog.form.reason"
|
||
type="textarea"
|
||
:rows="2"
|
||
maxlength="255"
|
||
show-word-limit
|
||
placeholder="请必填,例如:客户旺季临时授信 / 年审调整长期额度"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
</el-form>
|
||
|
||
<div class="credit-preview">
|
||
<div>调整后{{ creditDialog.form.creditType === 'TEMP' ? '临时额度' : '信用额度' }}:
|
||
<b :class="creditAfter < 0 ? 'money-out' : 'money-in'">{{ formatMoney(creditAfter) }}</b>
|
||
</div>
|
||
<div>调整后授信总额:<b>{{ formatMoney(totalCreditAfter) }}</b>
|
||
(可用 = 账户余额 {{ formatMoney(currentAccount.balance) }} + 授信总额 + 额外额度)</div>
|
||
<el-alert
|
||
v-if="creditAfter < 0"
|
||
type="error" :closable="false"
|
||
title="调整后额度不能为负数" class="credit-alert"
|
||
/>
|
||
</div>
|
||
|
||
<template #footer>
|
||
<el-button type="primary" :loading="creditSubmitLoading" @click="submitAdjustCredit">提交申请</el-button>
|
||
<el-button @click="creditDialog.visible = false">取 消</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- ====== 审批选择角色额度池弹窗 ====== -->
|
||
<el-dialog title="审批通过 - 选择额度池" v-model="approveDialog.visible" width="520px" append-to-body>
|
||
<el-alert type="warning" :closable="false" class="rate-tip" style="margin-bottom: 12px;">
|
||
本次为净增额度 {{ formatMoney(approveDialog.netDelta) }} CNY,需占用您名下角色的可分配额度总额;额度调减或到期后自动释放回池。
|
||
</el-alert>
|
||
<el-form label-width="100px">
|
||
<el-form-item label="额度池角色" required>
|
||
<el-select v-model="approveDialog.roleId" placeholder="请选择角色额度池" style="width: 100%;">
|
||
<el-option
|
||
v-for="p in approveDialog.pools"
|
||
:key="p.roleId"
|
||
:label="`${p.roleName}(总额 ${formatMoney(p.totalQuota)} / 已分配 ${formatMoney(p.usedQuota)} / 剩余 ${formatMoney(p.availableQuota)})`"
|
||
:value="p.roleId"
|
||
:disabled="Number(p.availableQuota) <= 0"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="本次需分配">
|
||
<span class="money-out">{{ formatMoney(approveDialog.netDelta) }} CNY</span>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button type="primary" :loading="approveDialog.loading" @click="submitApproveWithPool">确认审批通过</el-button>
|
||
<el-button @click="approveDialog.visible = false">取 消</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- ====== 登记收款-选择核销账单弹窗 ====== -->
|
||
<el-dialog title="选择待核销账单" v-model="billPicker.visible" width="860px" append-to-body>
|
||
<el-alert type="info" :closable="false" class="writeoff-summary">
|
||
当前仅支持人民币(CNY)账单核销,以下为状态已确认/部分核销的 CNY 账单;
|
||
已选 {{ billPicker.selected.length }} 张,合计未核销
|
||
<b class="money-out">{{ formatMoney(billPickerSelectedTotal) }} CNY</b>,
|
||
本收款单折算可核销上限 <b>{{ formatMoney(receiptBaseCny) }} CNY</b>
|
||
</el-alert>
|
||
<el-table
|
||
ref="billPickerTableRef"
|
||
:data="billPicker.rows"
|
||
border
|
||
size="small"
|
||
max-height="380"
|
||
row-key="arBillId"
|
||
v-loading="billPicker.loading"
|
||
@selection-change="onPickerSelectionChange"
|
||
>
|
||
<el-table-column type="selection" width="42" align="center" reserve-selection />
|
||
<el-table-column label="账单号" prop="billNo" align="center" width="160" show-overflow-tooltip />
|
||
<el-table-column label="账单名称" prop="billName" align="center" min-width="150" show-overflow-tooltip />
|
||
<el-table-column label="账单日期" prop="billDate" align="center" width="105">
|
||
<template #default="scope">{{ formatDate(scope.row.billDate) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="应收金额" align="center" width="110">
|
||
<template #default="scope">{{ formatMoney(scope.row.totalAmount) }} {{ scope.row.currency }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="已核销" align="center" width="100">
|
||
<template #default="scope"><span class="money-in">{{ formatMoney(scope.row.writtenOffAmount) }}</span></template>
|
||
</el-table-column>
|
||
<el-table-column label="未核销" align="center" width="100">
|
||
<template #default="scope"><span class="money-out">{{ formatMoney(scope.row.unwrittenAmount) }}</span></template>
|
||
</el-table-column>
|
||
<template #empty>
|
||
<el-empty description="该币种暂无可核销账单" :image-size="70" />
|
||
</template>
|
||
</el-table>
|
||
<template #footer>
|
||
<el-button type="primary" @click="confirmBillPicker">确 定({{ billPicker.selected.length }})</el-button>
|
||
<el-button @click="billPicker.visible = false">取 消</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- ====== 余额核销账单弹窗 ====== -->
|
||
<el-dialog title="余额核销账单" v-model="woDialog.visible" width="560px" append-to-body v-loading="woDialog.accountLoading">
|
||
<el-descriptions :column="1" border size="small" v-if="woDialog.bill">
|
||
<el-descriptions-item label="账单号">{{ woDialog.bill.billNo }}</el-descriptions-item>
|
||
<el-descriptions-item label="账单名称">{{ woDialog.bill.billName || '-' }}</el-descriptions-item>
|
||
<el-descriptions-item label="账单未核销金额">
|
||
<span class="money-out">CNY {{ formatMoney(woDialog.bill.unwrittenAmount) }}</span>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="账户可用余额">
|
||
<span class="money-in">CNY {{ formatMoney(currentAccount.balance) }}</span>
|
||
</el-descriptions-item>
|
||
</el-descriptions>
|
||
<el-form :model="woDialog.form" label-width="110px" class="wo-form">
|
||
<el-form-item label="本次核销金额" required>
|
||
<el-input
|
||
v-model="woDialog.form.writeoffAmount"
|
||
inputmode="decimal"
|
||
clearable
|
||
style="width: 220px;"
|
||
placeholder="请输入核销金额"
|
||
/>
|
||
<span class="wo-form-unit">CNY</span>
|
||
</el-form-item>
|
||
<el-form-item label="核销日期">
|
||
<el-date-picker
|
||
v-model="woDialog.form.writeoffDate"
|
||
type="date"
|
||
value-format="YYYY-MM-DD"
|
||
placeholder="默认今天"
|
||
style="width: 220px;"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="备注">
|
||
<el-input v-model="woDialog.form.remark" type="textarea" :rows="2" maxlength="255" show-word-limit
|
||
placeholder="选填,最多255字" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<el-alert type="info" :closable="false">
|
||
<template title>
|
||
核销仅更新账单已核销/未核销金额与状态,不再扣减账户余额;资金变动由收款/计费入口产生。
|
||
</template>
|
||
</el-alert>
|
||
<template #footer>
|
||
<el-button type="primary" :loading="woSubmitLoading" @click="submitWriteoff">确认核销</el-button>
|
||
<el-button @click="woDialog.visible = false">取 消</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- ====== 反核销弹窗 ====== -->
|
||
<el-dialog title="反核销(取消余额核销)" v-model="cancelWoDialog.visible" width="480px" append-to-body>
|
||
<el-alert type="warning" :closable="false" class="credit-tip">
|
||
<span v-if="cancelWoDialog.row">
|
||
反核销后,账单「{{ cancelWoDialog.row.billNo }}」恢复为未核销状态(核销金额 CNY {{ formatMoney(cancelWoDialog.row.writeoffAmount) }});
|
||
不回补账户余额、不变动已用额度。
|
||
<template v-if="cancelWoDialog.row.receiptId">
|
||
<br />该核销由收款单审核<strong>联动产生</strong>,反核销后收款单中本笔将回到“未分配”金额,可重新核销其他账单,已入账余额不受影响。
|
||
</template>
|
||
</span>
|
||
</el-alert>
|
||
<el-form label-width="100px" class="wo-form">
|
||
<el-form-item label="反核销原因" required>
|
||
<el-input v-model="cancelWoDialog.cancelRemark" type="textarea" :rows="3" maxlength="255" show-word-limit
|
||
placeholder="请填写反核销原因(必填)" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button type="warning" :loading="cancelWoLoading" @click="submitCancelWriteoff">确认反核销</el-button>
|
||
<el-button @click="cancelWoDialog.visible = false">取 消</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup name="CustomerMoney">
|
||
import axios from 'axios'
|
||
import { saveAs } from 'file-saver'
|
||
import CustomerBillListDialog from './CustomerBillListDialog.vue'
|
||
import { getCustomer } from "@/api/customersuppliermanagement/customerinfo/customer.js"
|
||
import { listAccount, listReceipt, getReceipt, addReceipt, confirmReceipt, cancelReceipt, listFlow, listCreditFlow, applyCreditAdjust, listCreditApply, approveCreditApply, rejectCreditApply, cancelCreditApply, listMyCreditPools } from "@/api/customersuppliermanagement/customermoney/customermoney.js"
|
||
import { listUnsettledBill, listBalanceWriteoff, balanceWriteoff, cancelBalanceWriteoff } from "@/api/customersuppliermanagement/customermoney/customerbill.js"
|
||
import { convertToBase } from "@/api/data/exchangeRate"
|
||
import { getToken } from "@/utils/auth"
|
||
import { AR_BALANCE_WRITEOFF_STATUS, AR_BALANCE_WRITEOFF_STATUS_OPTIONS, AR_BALANCE_WRITEOFF_STATUS_TYPES, AR_BILL_STATUS_OPTIONS, AR_BILL_STATUS_TYPES, arEnumLabel } from '@/constants/ar/enums'
|
||
|
||
import { useRoute } from 'vue-router'
|
||
import { nextTick } from 'vue'
|
||
import useUserStore from '@/store/modules/user'
|
||
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
const { proxy } = getCurrentInstance()
|
||
const baseUrl = import.meta.env.VITE_APP_BASE_API
|
||
const userStore = useUserStore()
|
||
/** 超管(admin 角色)审批额度不占用角色额度池,不受池余额限制 */
|
||
const isSuperAdmin = computed(() => (userStore.roles || []).includes('admin'))
|
||
|
||
const activeTab = ref('receipt')
|
||
const customer = ref({})
|
||
|
||
/* ================= 枚举 ================= */
|
||
const receiptTypeOptions = [
|
||
{ value: 'PREPAY', label: '预充值' },
|
||
{ value: 'REPAY', label: '还款核销' },
|
||
{ value: 'MIXED', label: '混合' }
|
||
]
|
||
const receiptStatusOptions = [
|
||
{ value: 'PENDING', label: '待确认' },
|
||
{ value: 'CONFIRMED', label: '已确认' },
|
||
{ value: 'CANCELLED', label: '已取消' }
|
||
]
|
||
const payMethodOptions = [
|
||
{ value: 'BANK_TRANSFER', label: '银行转账' },
|
||
{ value: 'ALIPAY', label: '支付宝' },
|
||
{ value: 'WECHAT', label: '微信' },
|
||
{ value: 'CASH', label: '现金' },
|
||
{ value: 'OTHER', label: '其他' }
|
||
]
|
||
const bizTypeOptions = [
|
||
{ value: 'RECEIPT', label: '收款' },
|
||
{ value: 'AR_BILL', label: '应收账单' },
|
||
{ value: 'BILL_PAY', label: '账单核销' },
|
||
{ value: 'ORDER', label: '计费订单' },
|
||
{ value: 'REFUND', label: '退款' },
|
||
{ value: 'ADJUST', label: '手工调整' },
|
||
{ value: 'FREEZE', label: '冻结' },
|
||
{ value: 'UNFREEZE', label: '解冻' }
|
||
]
|
||
/** 登记收款可选币种(汇率需在 数据管理-汇率管理 维护对CNY汇率) */
|
||
const currencyOptions = [
|
||
{ value: 'CNY', label: '人民币 CNY' },
|
||
{ value: 'USD', label: '美元 USD' },
|
||
{ value: 'EUR', label: '欧元 EUR' },
|
||
{ value: 'GBP', label: '英镑 GBP' },
|
||
{ value: 'HKD', label: '港币 HKD' },
|
||
{ value: 'JPY', label: '日元 JPY' }
|
||
]
|
||
|
||
|
||
|
||
function formatOption(options, value) {
|
||
if (value === null || value === undefined || value === '') return '-'
|
||
const opt = options.find(o => String(o.value) === String(value))
|
||
return opt ? opt.label : value
|
||
}
|
||
function receiptTypeTag(v) {
|
||
return { PREPAY: '', REPAY: 'warning', MIXED: 'info' }[v] || 'info'
|
||
}
|
||
function receiptStatusTag(v) {
|
||
return { PENDING: 'warning', CONFIRMED: 'success', CANCELLED: 'danger' }[v] || 'info'
|
||
}
|
||
const bizTypeTag = function(v) {
|
||
return { RECEIPT: 'success', AR_BILL: 'primary', BILL_PAY: 'danger', ORDER: 'warning', REFUND: 'warning', ADJUST: 'info', FREEZE: 'info', UNFREEZE: '' }[v] || 'info'
|
||
}
|
||
|
||
/* ================= 额度相关枚举 ================= */
|
||
const creditTypeOptions = [
|
||
{ value: 'CREDIT', label: '信用额度' },
|
||
{ value: 'TEMP', label: '临时额度' }
|
||
]
|
||
const creditChangeTypeOptions = [
|
||
{ value: 'INIT', label: '初始化' },
|
||
{ value: 'ADJUST', label: '手工调整' },
|
||
{ value: 'EXPIRE', label: '到期失效' },
|
||
{ value: 'OCCUPY', label: '授信占用' },
|
||
{ value: 'RELEASE', label: '授信释放' }
|
||
]
|
||
const adjustModeOptions = [
|
||
{ value: 'SET', label: '设置为' },
|
||
{ value: 'ADD', label: '增加' },
|
||
{ value: 'SUB', label: '减少' }
|
||
]
|
||
function creditChangeTag(v) {
|
||
return { INIT: '', ADJUST: 'primary', EXPIRE: 'danger', OCCUPY: 'warning', RELEASE: 'success' }[v] || 'info'
|
||
}
|
||
/**
|
||
* 是否为授信占用/释放流水(历史数据)。
|
||
* 口径变更(2026-09-23)后计费不再产生 OCCUPY/RELEASE 流水,改为写余额流水;
|
||
* 历史占用/释放流水的 变动前/变动额/变动后 记的是【已用额度】维度,
|
||
* INIT/ADJUST/EXPIRE 流水这三列记的是【授信额度】(信用/临时额度)维度。
|
||
*/
|
||
function isCreditUsageFlow(changeType) {
|
||
return changeType === 'OCCUPY' || changeType === 'RELEASE'
|
||
}
|
||
/** 收款汇率来源文案:RATE_TABLE汇率表 / MANUAL手工修改 / SYSTEM系统默认(本币) */
|
||
function receiptRateSourceLabel(v) {
|
||
return { RATE_TABLE: '汇率表', MANUAL: '手工修改', SYSTEM: '系统默认' }[v] || '系统默认'
|
||
}
|
||
/** 收款汇率来源标签颜色 */
|
||
function receiptRateSourceTag(v) {
|
||
return { RATE_TABLE: 'success', MANUAL: 'warning', SYSTEM: 'info' }[v] || 'info'
|
||
}
|
||
function formatMoney(val) {
|
||
const num = Number(val)
|
||
return isNaN(num) ? '0.00' : num.toFixed(2)
|
||
}
|
||
/** 汇率展示:去尾零,最多8位小数(7.10000000 → 7.1) */
|
||
function formatRate(val) {
|
||
if (val === null || val === undefined || val === '') return '1'
|
||
const num = Number(val)
|
||
return isNaN(num) ? String(val) : String(num)
|
||
}
|
||
function formatDateTime(val) {
|
||
if (!val) return '-'
|
||
return String(val).length > 16 ? String(val).substring(0, 16) : val
|
||
}
|
||
|
||
/* ================= 客户信息 & 账户(统一本位币 CNY) ================= */
|
||
const accountList = ref([])
|
||
|
||
const currentAccount = computed(() => {
|
||
return accountList.value.find(a => a.currency === 'CNY') || {}
|
||
})
|
||
|
||
/** 可用额度数值 = 账户余额 + 信用额度 + 额外额度(可能为负,表示欠费;口径变更后不再扣减已用额度) */
|
||
const availableCreditValue = computed(() => {
|
||
const a = currentAccount.value
|
||
const balance = Number(a.balance) || 0
|
||
const total = Number(a.totalCredit) || 0
|
||
const extra = Number(a.extraCredit) || 0
|
||
return balance + total + extra
|
||
})
|
||
/** 可用额度展示文本 */
|
||
const availableCredit = computed(() => formatMoney(availableCreditValue.value))
|
||
/** 是否处于欠费状态(可用额度为负;不拦截计费,允许负余额继续计费) */
|
||
const availableOverLimit = computed(() => availableCreditValue.value < 0)
|
||
|
||
/** 临时额度有效期文案 */
|
||
const tempCreditPeriodText = computed(() => {
|
||
const a = currentAccount.value
|
||
const extra = Number(a.extraCredit) || 0
|
||
if (extra <= 0) return ''
|
||
if (!a.extraExpireDate) return '长期有效'
|
||
const today = proxy.parseTime(new Date(), '{y}-{m}-{d}')
|
||
if (a.extraExpireDate < today) return '已过期'
|
||
return '有效期至 ' + a.extraExpireDate
|
||
})
|
||
const tempCreditPeriodExpired = computed(() => tempCreditPeriodText.value === '已过期')
|
||
|
||
/** 信用额度有效期文案 */
|
||
const creditPeriodText = computed(() => {
|
||
const a = currentAccount.value
|
||
const total = Number(a.totalCredit) || 0
|
||
if (total <= 0) return ''
|
||
if (!a.creditExpireDate) return '长期有效'
|
||
const today = proxy.parseTime(new Date(), '{y}-{m}-{d}')
|
||
if (a.creditExpireDate < today) return '已过期'
|
||
return '有效期至 ' + a.creditExpireDate
|
||
})
|
||
const creditPeriodExpired = computed(() => creditPeriodText.value === '已过期')
|
||
|
||
/* ============ 额度调整弹窗 ============ */
|
||
const creditFormRef = ref(null)
|
||
const creditSubmitLoading = ref(false)
|
||
const creditDialog = reactive({
|
||
visible: false,
|
||
form: {}
|
||
})
|
||
const creditRules = computed(() => ({
|
||
creditType: [{ required: true, message: '请选择额度类型', trigger: 'change' }],
|
||
adjustMode: [{ required: true, message: '请选择调整方式', trigger: 'change' }],
|
||
changeAmount: [{ required: true, message: '调整金额必须大于0', trigger: 'blur' }],
|
||
reason: [{ required: true, message: '请填写调整原因', trigger: 'blur' }],
|
||
expireDate: [{
|
||
validator: (rule, value, callback) => {
|
||
if ( !value) {
|
||
callback(new Error('请选择额度失效日期'))
|
||
} else {
|
||
callback()
|
||
}
|
||
},
|
||
trigger: 'change'
|
||
}]
|
||
}))
|
||
|
||
/** 禁止选择晚于今天的日期(不允许预约未来生效) */
|
||
function afterToday(date) {
|
||
const today = new Date()
|
||
today.setHours(23, 59, 59, 999)
|
||
return date.getTime() > today.getTime()
|
||
}
|
||
|
||
function defaultCreditForm(creditType) {
|
||
return {
|
||
customerId: route.query.customerId || null,
|
||
creditType: creditType || 'CREDIT',
|
||
adjustMode: 'ADD',
|
||
changeAmount: null,
|
||
reason: '',
|
||
effectiveDate: proxy.parseTime(new Date(), '{y}-{m}-{d}'),
|
||
expireDate: ''
|
||
}
|
||
}
|
||
|
||
function openAdjustDialog(creditType) {
|
||
const customerId = Number(route.query.customerId)
|
||
if (!customerId) {
|
||
proxy.$modal.msgError('缺少客户ID,请从客户列表进入客户余额页')
|
||
return
|
||
}
|
||
resetCreditForm()
|
||
creditDialog.form = defaultCreditForm(creditType)
|
||
creditDialog.visible = true
|
||
}
|
||
|
||
function resetCreditForm() {
|
||
creditDialog.form = defaultCreditForm()
|
||
proxy.resetForm('creditFormRef')
|
||
}
|
||
|
||
/** 切换额度类型:清掉校验红框,临时额度默认生效日为今天 */
|
||
function onCreditTypeChange() {
|
||
creditDialog.form.changeAmount = null
|
||
if (creditDialog.form.creditType === 'TEMP' && !creditDialog.form.effectiveDate) {
|
||
creditDialog.form.effectiveDate = proxy.parseTime(new Date(), '{y}-{m}-{d}')
|
||
}
|
||
nextTick(() => proxy.clearValidate('creditFormRef'))
|
||
}
|
||
|
||
/** 当前被调整额度(调整前) */
|
||
const creditBefore = computed(() => {
|
||
const a = currentAccount.value
|
||
return Number(creditDialog.form.creditType === 'TEMP' ? a.extraCredit : a.totalCredit) || 0
|
||
})
|
||
|
||
/** 调整后被调额度 */
|
||
const creditAfter = computed(() => {
|
||
const input = Number(creditDialog.form.changeAmount) || 0
|
||
const mode = creditDialog.form.adjustMode
|
||
if (mode === 'SET') return input
|
||
return mode === 'SUB' ? creditBefore.value - input : creditBefore.value + input
|
||
})
|
||
|
||
/** 调整后授信总额(信用+临时) */
|
||
const totalCreditAfter = computed(() => {
|
||
const a = currentAccount.value
|
||
const other = Number(creditDialog.form.creditType === 'TEMP' ? a.totalCredit : a.extraCredit) || 0
|
||
return other + creditAfter.value
|
||
})
|
||
|
||
function submitAdjustCredit() {
|
||
creditFormRef.value.validate(valid => {
|
||
if (!valid) return
|
||
const f = creditDialog.form
|
||
const amount = Number(f.changeAmount)
|
||
if (!(amount > 0)) {
|
||
proxy.$modal.msgError('调整金额必须大于0')
|
||
return
|
||
}
|
||
if (!f.expireDate) {
|
||
proxy.$modal.msgError(f.creditType === 'TEMP' ? '请选择临时额度失效日期' : '请选择信用额度失效日期')
|
||
return
|
||
}
|
||
if (f.expireDate && f.effectiveDate && f.expireDate <= f.effectiveDate) {
|
||
proxy.$modal.msgError('失效日期必须晚于生效日期')
|
||
return
|
||
}
|
||
if (creditAfter.value < 0) {
|
||
proxy.$modal.msgError('调整后额度不能为负数')
|
||
return
|
||
}
|
||
if (creditAfter.value === creditBefore.value) {
|
||
proxy.$modal.msgError('额度未发生变化,无需调整')
|
||
return
|
||
}
|
||
const payload = {
|
||
customerId: Number(f.customerId) || Number(route.query.customerId) || null,
|
||
creditType: f.creditType,
|
||
adjustMode: f.adjustMode,
|
||
changeAmount: amount,
|
||
reason: f.reason
|
||
}
|
||
if (!payload.customerId) {
|
||
proxy.$modal.msgError('缺少客户ID,请从客户列表进入客户余额页')
|
||
return
|
||
}
|
||
payload.effectiveDate = f.effectiveDate || null
|
||
payload.expireDate = f.expireDate || null
|
||
creditSubmitLoading.value = true
|
||
applyCreditAdjust(payload).then(() => {
|
||
proxy.$modal.msgSuccess('申请已提交,等待审批')
|
||
creditDialog.visible = false
|
||
if (creditApplyLoaded) {
|
||
getApplyList()
|
||
}
|
||
}).finally(() => {
|
||
creditSubmitLoading.value = false
|
||
})
|
||
})
|
||
}
|
||
|
||
function loadCustomer() {
|
||
const customerId = route.query.customerId
|
||
if (!customerId) return
|
||
getCustomer(customerId).then(res => {
|
||
customer.value = res.data || {}
|
||
})
|
||
}
|
||
|
||
function loadAccounts() {
|
||
const customerId = route.query.customerId
|
||
if (!customerId) return
|
||
listAccount(customerId).then(res => {
|
||
accountList.value = res.data || []
|
||
})
|
||
}
|
||
|
||
function goBack() {
|
||
router.push({ path: '/customersuppliermanagement/customerinfo' })
|
||
}
|
||
|
||
/* ================= 收款记录 ================= */
|
||
const receiptList = ref([])
|
||
const receiptTotal = ref(0)
|
||
const receiptLoading = ref(false)
|
||
const receiptDateRange = ref([])
|
||
const receiptQuery = reactive({
|
||
pageNum: 1,
|
||
pageSize: 10,
|
||
customerId: route.query.customerId,
|
||
receiptType: '',
|
||
status: ''
|
||
})
|
||
|
||
function getReceiptList() {
|
||
receiptLoading.value = true
|
||
const params = proxy.addDateRange({ ...receiptQuery }, receiptDateRange.value)
|
||
listReceipt(params).then(res => {
|
||
receiptList.value = res.rows || []
|
||
receiptTotal.value = res.total || 0
|
||
}).finally(() => {
|
||
receiptLoading.value = false
|
||
})
|
||
}
|
||
|
||
function resetReceiptQuery() {
|
||
receiptQuery.receiptType = ''
|
||
receiptQuery.status = ''
|
||
receiptDateRange.value = []
|
||
receiptQuery.pageNum = 1
|
||
getReceiptList()
|
||
}
|
||
|
||
/** 导出收款记录(按当前筛选,全量导出) */
|
||
function handleExportReceipt() {
|
||
const cid = route.query.customerId
|
||
if (!cid) return
|
||
const params = proxy.addDateRange({ ...receiptQuery }, receiptDateRange.value)
|
||
delete params.pageNum
|
||
delete params.pageSize
|
||
proxy.download('customersupplier/customermoney/receipt/export', params,
|
||
`收款记录_${new Date().getTime()}.xlsx`)
|
||
}
|
||
|
||
/** 导出余额流水(按当前筛选,全量导出) */
|
||
function handleExportFlow() {
|
||
const cid = route.query.customerId
|
||
if (!cid) return
|
||
const params = proxy.addDateRange({ ...flowQuery }, flowDateRange.value)
|
||
delete params.pageNum
|
||
delete params.pageSize
|
||
proxy.download('customersupplier/customermoney/flow/export', params,
|
||
`余额流水_${new Date().getTime()}.xlsx`)
|
||
}
|
||
|
||
/** 导出额度流水(按当前筛选,全量导出) */
|
||
function handleExportCreditFlow() {
|
||
const cid = route.query.customerId
|
||
if (!cid) return
|
||
const params = proxy.addDateRange({ ...creditFlowQuery }, creditFlowDateRange.value)
|
||
delete params.pageNum
|
||
delete params.pageSize
|
||
proxy.download('customersupplier/customermoney/credit/flow/export', params,
|
||
`额度流水_${new Date().getTime()}.xlsx`)
|
||
}
|
||
|
||
/** 审核通过 */
|
||
function handleConfirmReceipt(row) {
|
||
proxy.$modal.confirm(`是否确认审核通过收款单「${row.receiptNo}」?审核通过后金额计入客户余额,登记时选择的账单将一并核销。`).then(() => {
|
||
return confirmReceipt(row.id)
|
||
}).then(() => {
|
||
proxy.$modal.msgSuccess("审核通过,余额与账单核销已更新")
|
||
getReceiptList()
|
||
loadAccounts()
|
||
loadBillWriteoffData()
|
||
if (activeTab.value === 'flow') getFlowList()
|
||
}).catch(() => {})
|
||
}
|
||
|
||
/** 取消收款单 */
|
||
function handleCancelReceipt(row) {
|
||
proxy.$modal.confirm(`是否确认取消收款单「${row.receiptNo}」?取消后不可恢复。`).then(() => {
|
||
return cancelReceipt(row.id)
|
||
}).then(() => {
|
||
proxy.$modal.msgSuccess("已取消")
|
||
getReceiptList()
|
||
}).catch(() => {})
|
||
}
|
||
|
||
/* ============ 登记收款弹窗 ============ */
|
||
const receiptFormRef = ref(null)
|
||
const submitLoading = ref(false)
|
||
const receiptDialog = reactive({
|
||
visible: false,
|
||
form: {}
|
||
})
|
||
const receiptRules = {
|
||
receivedAt: [{ required: true, message: '收款日期不能为空', trigger: 'change' }],
|
||
amount: [{ required: true, message: '收款金额不能为空', trigger: 'blur' }],
|
||
receiptType: [{ required: true, message: '请选择收款类型', trigger: 'change' }],
|
||
currency: [{ required: true, message: '请选择币种', trigger: 'change' }]
|
||
}
|
||
|
||
function resetReceiptForm() {
|
||
rateEdited.value = false
|
||
rateSourceInfo.value = { source: '', ratePath: '' }
|
||
receiptDialog.form = {
|
||
customerId: route.query.customerId || null,
|
||
receivedAt: proxy.parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}'),
|
||
amount: '',
|
||
receiptType: 'PREPAY',
|
||
currency: 'CNY',
|
||
exchangeRate: 1,
|
||
payMethod: 'BANK_TRANSFER',
|
||
bankAccountNo: '',
|
||
bankTransactionNo: '',
|
||
remark: '',
|
||
attachmentList: [],
|
||
writeoffPlans: []
|
||
}
|
||
proxy.resetForm('receiptFormRef')
|
||
}
|
||
|
||
function handleAddReceipt() {
|
||
resetReceiptForm()
|
||
receiptDialog.visible = true
|
||
}
|
||
|
||
/** 汇率是否被手工修改过(修改后切换币种/日期不再自动覆盖) */
|
||
const rateEdited = ref(false)
|
||
/** 汇率表返回的来源信息(source: 中国银行等;ratePath: DIRECT/REVERSE/CROSS),仅用于表单实时展示 */
|
||
const rateSourceInfo = ref({ source: '', ratePath: '' })
|
||
|
||
/** 按收款日期从汇率表取该币种对CNY即期汇率 */
|
||
function fetchDefaultRate() {
|
||
const cur = receiptDialog.form.currency
|
||
if (!cur || cur === 'CNY') {
|
||
receiptDialog.form.exchangeRate = 1
|
||
rateSourceInfo.value = { source: '', ratePath: 'SAME' }
|
||
return
|
||
}
|
||
const bizDate = receiptDialog.form.receivedAt
|
||
? String(receiptDialog.form.receivedAt).substring(0, 10)
|
||
: proxy.parseTime(new Date(), '{y}-{m}-{d}')
|
||
convertToBase({ amount: 1, currency: cur, rateType: '0', bizDate })
|
||
.then(res => {
|
||
if (!rateEdited.value) {
|
||
receiptDialog.form.exchangeRate = Number(res.data.rate)
|
||
rateSourceInfo.value = { source: res.data.source || '', ratePath: res.data.ratePath || 'DIRECT' }
|
||
}
|
||
})
|
||
.catch(() => {
|
||
if (!rateEdited.value) {
|
||
receiptDialog.form.exchangeRate = undefined
|
||
rateSourceInfo.value = { source: '', ratePath: '' }
|
||
proxy.$modal.msgWarning(`汇率表中未维护 ${cur} 对 CNY 的汇率,请手工录入汇率`)
|
||
}
|
||
})
|
||
}
|
||
|
||
/** 切换币种:重置手工标记并重新取默认汇率;核销账单固定为CNY,只需按新汇率重算可核销上限,不清空已选 */
|
||
function handleCurrencyChange() {
|
||
rateEdited.value = false
|
||
rateSourceInfo.value = { source: '', ratePath: '' }
|
||
fetchDefaultRate()
|
||
}
|
||
|
||
/** 手工修改汇率 */
|
||
function handleRateInput() {
|
||
if (receiptDialog.form.currency !== 'CNY') rateEdited.value = true
|
||
}
|
||
|
||
/** 本位币金额预览 = 原币金额 × 汇率 */
|
||
const baseAmountPreview = computed(() => {
|
||
const amount = Number(receiptDialog.form.amount) || 0
|
||
const rate = Number(receiptDialog.form.exchangeRate) || 0
|
||
return formatMoney(amount * rate)
|
||
})
|
||
|
||
// 收款日期变化且汇率未手改时,按新日期重新取汇率表
|
||
watch(() => receiptDialog.form.receivedAt, () => {
|
||
if (receiptDialog.visible && receiptDialog.form.currency !== 'CNY' && !rateEdited.value) {
|
||
fetchDefaultRate()
|
||
}
|
||
})
|
||
|
||
/* ============ 登记收款-选择核销账单 ============ */
|
||
const billPickerTableRef = ref(null)
|
||
const billPicker = reactive({
|
||
visible: false,
|
||
loading: false,
|
||
rows: [],
|
||
selected: []
|
||
})
|
||
|
||
/** 收款金额按汇率折算的本位币金额(CNY),即核销合计上限 */
|
||
const receiptBaseCny = computed(() => {
|
||
const amt = Number(receiptDialog.form.amount) || 0
|
||
const rate = Number(receiptDialog.form.exchangeRate) || 0
|
||
return Math.round(amt * rate * 100) / 100
|
||
})
|
||
/** 已分配合计(账单均为CNY,此值即 CNY) */
|
||
const receiptWriteoffTotal = computed(() => {
|
||
const list = receiptDialog.form.writeoffPlans || []
|
||
return list.reduce((s, r) => s + (Number(r.writeoffAmount) || 0), 0)
|
||
})
|
||
/** 核销后剩余入账金额(折算CNY - 已分配CNY) */
|
||
const receiptWriteoffRemain = computed(() => {
|
||
return Math.round((receiptBaseCny.value - receiptWriteoffTotal.value) * 100) / 100
|
||
})
|
||
/** 选择器中勾选账单的未核销额合计(仅提示) */
|
||
const billPickerSelectedTotal = computed(() => {
|
||
return billPicker.selected.reduce((s, r) => s + (Number(r.unwrittenAmount) || 0), 0)
|
||
})
|
||
|
||
const RECEIPT_PLAN_STATUS_MAP = {
|
||
PENDING: { label: '待核销', type: 'warning' },
|
||
EFFECTIVE: { label: '已核销', type: 'success' },
|
||
CANCELLED: { label: '已作废', type: 'info' },
|
||
REVERSED: { label: '已反核销', type: 'danger' }
|
||
}
|
||
function receiptPlanStatusLabel(s) {
|
||
return RECEIPT_PLAN_STATUS_MAP[s]?.label || s || '-'
|
||
}
|
||
function receiptPlanStatusTag(s) {
|
||
return RECEIPT_PLAN_STATUS_MAP[s]?.type || 'info'
|
||
}
|
||
|
||
/** 打开账单选择器:只列与收款同币种的未核销账单,并回显已选 */
|
||
function openBillPicker() {
|
||
const customerId = route.query.customerId
|
||
if (!customerId) {
|
||
proxy.$modal.msgError('缺少客户信息,请重新进入页面')
|
||
return
|
||
}
|
||
billPicker.visible = true
|
||
billPicker.loading = true
|
||
billPicker.rows = []
|
||
billPicker.selected = []
|
||
listUnsettledBill(customerId).then(res => {
|
||
// 现阶段只核销人民币账单,与收款币种无关;核销上限为收款折算 CNY
|
||
billPicker.rows = (res.data || []).filter(r => (r.currency || 'CNY') === 'CNY')
|
||
nextTick(() => {
|
||
billPickerTableRef.value?.clearSelection()
|
||
const chosen = receiptDialog.form.writeoffPlans || []
|
||
billPicker.rows.forEach(r => {
|
||
if (chosen.some(p => p.arBillId === r.arBillId)) {
|
||
billPickerTableRef.value?.toggleRowSelection(r, true)
|
||
}
|
||
})
|
||
})
|
||
}).finally(() => {
|
||
billPicker.loading = false
|
||
})
|
||
}
|
||
|
||
function onPickerSelectionChange(rows) {
|
||
billPicker.selected = rows || []
|
||
}
|
||
|
||
/** 确认选择:新增行默认核销额=min(账单未核销额CNY, 收款折算CNY剩余可用),保留已填金额 */
|
||
function confirmBillPicker() {
|
||
const oldMap = new Map((receiptDialog.form.writeoffPlans || []).map(p => [p.arBillId, p]))
|
||
receiptDialog.form.writeoffPlans = billPicker.selected.map(r => {
|
||
const existed = oldMap.get(r.arBillId)
|
||
const unwritten = Number(r.unwrittenAmount) || 0
|
||
const usedByOthers = receiptDialog.form.writeoffPlans
|
||
.filter(p => p.arBillId !== r.arBillId)
|
||
.reduce((s, p) => s + (Number(p.writeoffAmount) || 0), 0)
|
||
const remainCny = Math.max(receiptBaseCny.value - usedByOthers, 0)
|
||
return {
|
||
arBillId: r.arBillId,
|
||
billNo: r.billNo,
|
||
billName: r.billName,
|
||
currency: 'CNY',
|
||
unwrittenAmount: unwritten,
|
||
writeoffAmount: existed ? Number(existed.writeoffAmount) : Math.round(Math.min(unwritten, remainCny) * 100) / 100
|
||
}
|
||
})
|
||
billPicker.visible = false
|
||
}
|
||
|
||
function removeReceiptBill(index) {
|
||
receiptDialog.form.writeoffPlans.splice(index, 1)
|
||
}
|
||
|
||
/** 提交前校验核销账单(金额口径均为 CNY) */
|
||
function validateReceiptWriteoffPlans() {
|
||
const cap = receiptBaseCny.value
|
||
const list = receiptDialog.form.writeoffPlans || []
|
||
let total = 0
|
||
for (const p of list) {
|
||
const amt = Number(p.writeoffAmount)
|
||
if (!amt || amt <= 0) {
|
||
proxy.$modal.msgError(`账单【${p.billNo}】本次核销金额必须大于0`)
|
||
return false
|
||
}
|
||
if (amt > Number(p.unwrittenAmount)) {
|
||
proxy.$modal.msgError(`账单【${p.billNo}】核销金额超过其未核销金额`)
|
||
return false
|
||
}
|
||
total += amt
|
||
}
|
||
if (Math.round(total * 100) > Math.round(cap * 100)) {
|
||
proxy.$modal.msgError(`核销金额合计 ${total.toFixed(2)} CNY 超过收款折算本位币金额 ${cap.toFixed(2)} CNY`)
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
function submitReceipt() {
|
||
receiptFormRef.value.validate(valid => {
|
||
if (!valid) return
|
||
const cur = receiptDialog.form.currency
|
||
const rate = Number(receiptDialog.form.exchangeRate)
|
||
if (cur !== 'CNY' && (!rate || rate <= 0)) {
|
||
proxy.$modal.msgError(`请录入 ${cur} 对 CNY 的汇率(汇率表未配置时可手工填写)`)
|
||
return
|
||
}
|
||
if (!validateReceiptWriteoffPlans()) return
|
||
submitLoading.value = true
|
||
addReceipt(receiptDialog.form).then(() => {
|
||
proxy.$modal.msgSuccess("登记成功,等待审核")
|
||
receiptDialog.visible = false
|
||
getReceiptList()
|
||
}).finally(() => {
|
||
submitLoading.value = false
|
||
})
|
||
})
|
||
}
|
||
|
||
function onReceiptDialogClosed() {
|
||
resetReceiptForm()
|
||
}
|
||
|
||
/* ============ 收款单查看弹窗 ============ */
|
||
const viewDialog = reactive({
|
||
visible: false,
|
||
data: null,
|
||
attachments: [],
|
||
loading: false
|
||
})
|
||
let viewReceiptId = null
|
||
|
||
function handleViewReceipt(row) {
|
||
viewReceiptId = row.id
|
||
viewDialog.visible = true
|
||
viewDialog.loading = true
|
||
viewDialog.data = null
|
||
viewDialog.attachments = []
|
||
getReceipt(row.id).then(res => {
|
||
viewDialog.data = res.data || null
|
||
viewDialog.attachments = res.data?.attachmentList || []
|
||
}).finally(() => {
|
||
viewDialog.loading = false
|
||
})
|
||
}
|
||
|
||
function handleConfirmFromView() {
|
||
if (!viewReceiptId) return
|
||
viewDialog.visible = false
|
||
const row = { id: viewReceiptId, receiptNo: viewDialog.data?.receiptNo }
|
||
handleConfirmReceipt(row)
|
||
}
|
||
|
||
/* ================= 余额流水 ================= */
|
||
const flowList = ref([])
|
||
const flowTotal = ref(0)
|
||
const flowLoading = ref(false)
|
||
const flowDateRange = ref([])
|
||
const flowQuery = reactive({
|
||
pageNum: 1,
|
||
pageSize: 10,
|
||
customerId: route.query.customerId,
|
||
bizType: '',
|
||
direction: ''
|
||
})
|
||
|
||
function getFlowList() {
|
||
flowLoading.value = true
|
||
const params = proxy.addDateRange({ ...flowQuery }, flowDateRange.value)
|
||
listFlow(params).then(res => {
|
||
flowList.value = res.rows || []
|
||
flowTotal.value = res.total || 0
|
||
}).finally(() => {
|
||
flowLoading.value = false
|
||
})
|
||
}
|
||
|
||
function resetFlowQuery() {
|
||
flowQuery.bizType = ''
|
||
flowQuery.direction = ''
|
||
flowDateRange.value = []
|
||
flowQuery.pageNum = 1
|
||
getFlowList()
|
||
}
|
||
|
||
/* ================= 额度流水 ================= */
|
||
const creditFlowList = ref([])
|
||
const creditFlowTotal = ref(0)
|
||
const creditFlowLoading = ref(false)
|
||
const creditFlowDateRange = ref([])
|
||
const creditFlowQuery = reactive({
|
||
pageNum: 1,
|
||
pageSize: 10,
|
||
customerId: route.query.customerId,
|
||
creditType: '',
|
||
changeType: ''
|
||
})
|
||
|
||
function getCreditFlowList() {
|
||
creditFlowLoading.value = true
|
||
const params = proxy.addDateRange({ ...creditFlowQuery }, creditFlowDateRange.value)
|
||
listCreditFlow(params).then(res => {
|
||
creditFlowList.value = res.rows || []
|
||
creditFlowTotal.value = res.total || 0
|
||
}).finally(() => {
|
||
creditFlowLoading.value = false
|
||
})
|
||
}
|
||
|
||
function resetCreditFlowQuery() {
|
||
creditFlowQuery.creditType = ''
|
||
creditFlowQuery.changeType = ''
|
||
creditFlowDateRange.value = []
|
||
creditFlowQuery.pageNum = 1
|
||
getCreditFlowList()
|
||
}
|
||
|
||
/* ================= 额度调整申请/审批 ================= */
|
||
const applyStatusOptions = [
|
||
{ value: 'PENDING', label: '待审批' },
|
||
{ value: 'APPROVED', label: '已通过' },
|
||
{ value: 'REJECTED', label: '已驳回' },
|
||
{ value: 'CANCELLED', label: '已撤销' }
|
||
]
|
||
const applyStatusTagMap = { PENDING: 'warning', APPROVED: 'success', REJECTED: 'danger', CANCELLED: 'info' }
|
||
function applyStatusTag(v) {
|
||
return applyStatusTagMap[v] || 'info'
|
||
}
|
||
|
||
const applyList = ref([])
|
||
const applyTotal = ref(0)
|
||
const applyLoading = ref(false)
|
||
const applyQuery = reactive({
|
||
pageNum: 1,
|
||
pageSize: 10,
|
||
customerId: route.query.customerId,
|
||
creditType: '',
|
||
status: ''
|
||
})
|
||
let creditApplyLoaded = false
|
||
|
||
function getApplyList() {
|
||
const customerId = route.query.customerId
|
||
if (!customerId) return
|
||
applyLoading.value = true
|
||
applyQuery.customerId = customerId
|
||
listCreditApply({ ...applyQuery }).then(res => {
|
||
applyList.value = res.rows || []
|
||
applyTotal.value = res.total || 0
|
||
}).finally(() => {
|
||
applyLoading.value = false
|
||
})
|
||
}
|
||
|
||
function resetApplyQuery() {
|
||
applyQuery.creditType = ''
|
||
applyQuery.status = ''
|
||
applyQuery.pageNum = 1
|
||
getApplyList()
|
||
}
|
||
|
||
/** 审批通过后刷新账户卡片与额度流水 */
|
||
function afterApplyHandled() {
|
||
getApplyList()
|
||
loadAccounts()
|
||
getCreditFlowList()
|
||
}
|
||
|
||
function isApplyNetIncrease(row) {
|
||
const a = currentAccount.value
|
||
const before = Number(row.creditType === 'TEMP' ? a.extraCredit : a.totalCredit) || 0
|
||
const input = Number(row.changeAmount) || 0
|
||
let after = before
|
||
if (row.adjustMode === 'SET') after = input
|
||
else if (row.adjustMode === 'ADD') after = before + input
|
||
else after = before - input
|
||
return after - before
|
||
}
|
||
|
||
function approveApply(row, approveRoleId) {
|
||
return approveCreditApply(row.applyId, { approveRemark: '审批通过', approveRoleId: approveRoleId || null })
|
||
}
|
||
|
||
function handleApproveApply(row) {
|
||
const netDelta = isApplyNetIncrease(row)
|
||
listMyCreditPools().then(res => {
|
||
const pools = res.data || []
|
||
// 超管审批不占用角色额度池,直接通过(后端 myPools 对超管返回空列表,此处双保险)
|
||
if (!isSuperAdmin.value && netDelta > 0 && pools.length > 0) {
|
||
// 非超管且名下有角色额度池:净增审批必须选池占用
|
||
approveDialog.apply = row
|
||
approveDialog.pools = pools
|
||
approveDialog.roleId = pools.length === 1 ? pools[0].roleId : null
|
||
approveDialog.netDelta = netDelta
|
||
approveDialog.visible = true
|
||
return
|
||
}
|
||
const poolTip = (!isSuperAdmin.value && netDelta > 0 && pools.length === 0)
|
||
? '(提示:净增额度需占用角色额度池,您名下未配置额度池,提交后将由后端校验拦截)'
|
||
: ''
|
||
proxy.$modal.confirm(`确认通过额度调整申请「${row.applyNo}」?通过后将立即调整客户额度并写入额度流水。${poolTip}`)
|
||
.then(() => approveApply(row, null))
|
||
.then(() => {
|
||
proxy.$modal.msgSuccess('已审批通过')
|
||
afterApplyHandled()
|
||
}).catch(() => {})
|
||
}).catch(() => {})
|
||
}
|
||
|
||
/** 审批选角色额度池弹窗 */
|
||
const approveDialog = reactive({
|
||
visible: false,
|
||
loading: false,
|
||
apply: null,
|
||
pools: [],
|
||
roleId: null,
|
||
netDelta: 0
|
||
})
|
||
|
||
const selectedPool = computed(() => approveDialog.pools.find(p => p.roleId === approveDialog.roleId))
|
||
|
||
function submitApproveWithPool() {
|
||
if (!approveDialog.roleId) {
|
||
proxy.$modal.msgError('请选择本次额度分配占用的角色额度池')
|
||
return
|
||
}
|
||
const pool = selectedPool.value
|
||
if (pool && Number(pool.availableQuota) < Number(approveDialog.netDelta)) {
|
||
proxy.$modal.msgError(`角色【${pool.roleName}】额度池剩余 ${pool.availableQuota},本次需分配 ${formatMoney(approveDialog.netDelta)}`)
|
||
return
|
||
}
|
||
approveDialog.loading = true
|
||
approveApply(approveDialog.apply, approveDialog.roleId).then(() => {
|
||
proxy.$modal.msgSuccess('已审批通过')
|
||
approveDialog.visible = false
|
||
afterApplyHandled()
|
||
}).finally(() => {
|
||
approveDialog.loading = false
|
||
})
|
||
}
|
||
|
||
function handleRejectApply(row) {
|
||
proxy.$prompt('请输入驳回原因', '驳回额度调整申请', {
|
||
confirmButtonText: '确认驳回',
|
||
cancelButtonText: '取消',
|
||
inputType: 'textarea',
|
||
inputPattern: /\S+/,
|
||
inputErrorMessage: '驳回原因不能为空'
|
||
}).then(({ value }) => rejectCreditApply(row.applyId, value)).then(() => {
|
||
proxy.$modal.msgSuccess('已驳回')
|
||
getApplyList()
|
||
}).catch(() => {})
|
||
}
|
||
|
||
function handleCancelApply(row) {
|
||
proxy.$modal.confirm(`确认撤销待审批申请「${row.applyNo}」?`)
|
||
.then(() => cancelCreditApply(row.applyId))
|
||
.then(() => {
|
||
proxy.$modal.msgSuccess('已撤销')
|
||
getApplyList()
|
||
}).catch(() => {})
|
||
}
|
||
|
||
/* ================= 账单核销 ================= */
|
||
function billStatusLabel(v) {
|
||
return arEnumLabel(AR_BILL_STATUS_OPTIONS, v)
|
||
}
|
||
function billStatusTag(v) {
|
||
return AR_BILL_STATUS_TYPES[v] || 'info'
|
||
}
|
||
function formatDate(val) {
|
||
if (!val) return '-'
|
||
return String(val).substring(0, 10)
|
||
}
|
||
const billDialogVisible = ref(false)
|
||
const currentCustomerId = computed(() => {
|
||
const id = Number(route.query.customerId)
|
||
return Number.isSafeInteger(id) && id > 0 ? id : null
|
||
})
|
||
|
||
function openBillDialog() {
|
||
if (!currentCustomerId.value) {
|
||
proxy.$modal.msgWarning('缺少客户ID,请从客户列表进入客户余额页')
|
||
return
|
||
}
|
||
billDialogVisible.value = true
|
||
}
|
||
|
||
const unsettledList = ref([])
|
||
const unsettledLoading = ref(false)
|
||
/** 未核销账单合计 */
|
||
const unsettledTotal = computed(() => {
|
||
const sum = unsettledList.value.reduce((s, r) => s + (Number(r.unwrittenAmount) || 0), 0)
|
||
return formatMoney(sum)
|
||
})
|
||
|
||
const writeoffList = ref([])
|
||
const writeoffTotal = ref(0)
|
||
const writeoffLoading = ref(false)
|
||
const woQuery = reactive({
|
||
pageNum: 1,
|
||
pageSize: 10,
|
||
customerId: route.query.customerId,
|
||
status: ''
|
||
})
|
||
let billWriteoffLoaded = false
|
||
|
||
function loadUnsettledBills() {
|
||
const customerId = route.query.customerId
|
||
if (!customerId) return
|
||
unsettledLoading.value = true
|
||
listUnsettledBill(customerId).then(res => {
|
||
unsettledList.value = res.data || []
|
||
}).finally(() => {
|
||
unsettledLoading.value = false
|
||
})
|
||
}
|
||
|
||
function getWriteoffList() {
|
||
const customerId = route.query.customerId
|
||
if (!customerId) return
|
||
writeoffLoading.value = true
|
||
woQuery.customerId = customerId
|
||
listBalanceWriteoff({ ...woQuery }).then(res => {
|
||
writeoffList.value = res.rows || []
|
||
writeoffTotal.value = res.total || 0
|
||
}).finally(() => {
|
||
writeoffLoading.value = false
|
||
})
|
||
}
|
||
|
||
function resetWriteoffQuery() {
|
||
woQuery.status = ''
|
||
woQuery.pageNum = 1
|
||
getWriteoffList()
|
||
}
|
||
|
||
function loadBillWriteoffData() {
|
||
loadAccounts()
|
||
loadUnsettledBills()
|
||
getWriteoffList()
|
||
}
|
||
|
||
/* ---- 核销弹窗 ---- */
|
||
const woSubmitLoading = ref(false)
|
||
const woDialog = reactive({ visible: false, bill: null, form: {}, accountLoading: false })
|
||
let writeoffAccountRequestSerial = 0
|
||
|
||
async function openWriteoffDialog(row) {
|
||
const customerId = Number(route.query.customerId)
|
||
if (!customerId) {
|
||
proxy.$modal.msgError('缺少客户ID,无法发起核销')
|
||
return
|
||
}
|
||
|
||
woDialog.bill = row
|
||
const unwritten = Number(row.unwrittenAmount) || 0
|
||
// 口径变更后核销不再扣减余额,默认核销金额取账单未核销金额
|
||
woDialog.form = {
|
||
arBillId: row.arBillId,
|
||
customerId,
|
||
writeoffAmount: unwritten,
|
||
writeoffDate: proxy.parseTime(new Date(), '{y}-{m}-{d}'),
|
||
remark: ''
|
||
}
|
||
woDialog.visible = true
|
||
|
||
// 仍刷新账户余额供展示,但不再限制核销金额
|
||
const requestSerial = ++writeoffAccountRequestSerial
|
||
woDialog.accountLoading = true
|
||
try {
|
||
const res = await listAccount(customerId)
|
||
if (requestSerial !== writeoffAccountRequestSerial) return
|
||
accountList.value = res.data || []
|
||
} catch (e) {
|
||
if (requestSerial !== writeoffAccountRequestSerial) return
|
||
proxy.$modal.msgError('账户余额刷新失败,请稍后重试')
|
||
} finally {
|
||
if (requestSerial === writeoffAccountRequestSerial) woDialog.accountLoading = false
|
||
}
|
||
}
|
||
|
||
function submitWriteoff() {
|
||
if (woDialog.accountLoading) {
|
||
proxy.$modal.msgWarning('正在刷新账户余额,请稍候')
|
||
return
|
||
}
|
||
const bill = woDialog.bill
|
||
const rawAmount = typeof woDialog.form.writeoffAmount === 'string'
|
||
? woDialog.form.writeoffAmount.trim()
|
||
: woDialog.form.writeoffAmount
|
||
const amount = Number(rawAmount)
|
||
if (!(amount > 0)) {
|
||
proxy.$modal.msgError('核销金额必须大于0')
|
||
return
|
||
}
|
||
const unwritten = Number(bill.unwrittenAmount) || 0
|
||
if (amount > unwritten) {
|
||
proxy.$modal.msgError('核销金额不能超过账单未核销金额 ' + formatMoney(unwritten))
|
||
return
|
||
}
|
||
woSubmitLoading.value = true
|
||
balanceWriteoff(woDialog.form).then(() => {
|
||
proxy.$modal.msgSuccess('核销成功:账单已核销 CNY ' + formatMoney(amount))
|
||
woDialog.visible = false
|
||
loadBillWriteoffData()
|
||
getFlowList()
|
||
}).finally(() => {
|
||
woSubmitLoading.value = false
|
||
})
|
||
}
|
||
|
||
/* ---- 反核销弹窗 ---- */
|
||
const cancelWoLoading = ref(false)
|
||
const cancelWoDialog = reactive({ visible: false, row: null, cancelRemark: '' })
|
||
|
||
function openCancelWriteoffDialog(row) {
|
||
cancelWoDialog.row = row
|
||
cancelWoDialog.cancelRemark = ''
|
||
cancelWoDialog.visible = true
|
||
}
|
||
|
||
function submitCancelWriteoff() {
|
||
const remark = cancelWoDialog.cancelRemark?.trim()
|
||
if (!remark) {
|
||
proxy.$modal.msgError('请填写反核销原因')
|
||
return
|
||
}
|
||
cancelWoLoading.value = true
|
||
cancelBalanceWriteoff(cancelWoDialog.row.writeoffId, remark).then(() => {
|
||
proxy.$modal.msgSuccess('反核销成功,账单已恢复未核销')
|
||
cancelWoDialog.visible = false
|
||
loadBillWriteoffData()
|
||
getFlowList()
|
||
}).finally(() => {
|
||
cancelWoLoading.value = false
|
||
})
|
||
}
|
||
|
||
watch(activeTab, (tab) => {
|
||
if (tab === 'creditApply' && !creditApplyLoaded) {
|
||
creditApplyLoaded = true
|
||
getApplyList()
|
||
}
|
||
if (tab === 'billWriteoff' && !billWriteoffLoaded) {
|
||
billWriteoffLoaded = true
|
||
loadBillWriteoffData()
|
||
}
|
||
})
|
||
|
||
/* ================= 附件上传 ================= */
|
||
const uploadFileUrl = ref(baseUrl + "/common/upload/oss")
|
||
const uploadHeaders = ref({ Authorization: "Bearer " + getToken() })
|
||
const ATTACHMENT_SIZE_LIMIT = 50
|
||
const ATTACHMENT_EXTENSIONS = ["pdf", "jpg", "jpeg", "png", "gif", "doc", "docx", "xls", "xlsx", "zip", "rar"]
|
||
|
||
function formatFileSize(bytes) {
|
||
if (!bytes) return "0 B"
|
||
const k = 1024
|
||
const sizes = ["B", "KB", "MB", "GB"]
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]
|
||
}
|
||
|
||
function handleBeforeUpload(file) {
|
||
const ext = (file.name.split('.').pop() || "").toLowerCase()
|
||
if (ATTACHMENT_EXTENSIONS.indexOf(ext) < 0) {
|
||
proxy.$modal.msgError(`不支持的文件格式:${ext}`)
|
||
return false
|
||
}
|
||
if (file.name.includes(',')) {
|
||
proxy.$modal.msgError('文件名不正确,不能包含英文逗号!')
|
||
return false
|
||
}
|
||
if (file.size / 1024 / 1024 >= ATTACHMENT_SIZE_LIMIT) {
|
||
proxy.$modal.msgError(`上传文件大小不能超过 ${ATTACHMENT_SIZE_LIMIT} MB!`)
|
||
return false
|
||
}
|
||
proxy.$modal.loading("正在上传文件,请稍候...")
|
||
return true
|
||
}
|
||
|
||
function handleUploadSuccess(res, file) {
|
||
proxy.$modal.closeLoading()
|
||
if (res.code === 200) {
|
||
receiptDialog.form.attachmentList.push({
|
||
attachmentId: null,
|
||
customerId: route.query.customerId || null,
|
||
bizType: "customer_receipt",
|
||
bizId: null,
|
||
fileName: file.name,
|
||
filePath: res.fileName,
|
||
fileSize: file.size,
|
||
fileType: file.type || '',
|
||
fileExtension: (file.name.split('.').pop() || "").toLowerCase(),
|
||
attachmentType: "0",
|
||
isLogo: "0",
|
||
sortOrder: receiptDialog.form.attachmentList.length,
|
||
remark: "",
|
||
delFlag: "0"
|
||
})
|
||
} else {
|
||
proxy.$modal.msgError(res.msg || "上传文件失败")
|
||
}
|
||
}
|
||
|
||
function handleUploadError() {
|
||
proxy.$modal.closeLoading()
|
||
proxy.$modal.msgError("上传文件失败")
|
||
}
|
||
|
||
function handleRemoveUpload(index) {
|
||
receiptDialog.form.attachmentList.splice(index, 1)
|
||
}
|
||
|
||
/** 附件下载(携带鉴权头,并以原始文件名保存) */
|
||
function handleDownload(file) {
|
||
proxy.$modal.loading("正在下载文件,请稍候...")
|
||
axios({
|
||
method: 'get',
|
||
url: baseUrl + "/common/download/resource?resource=" + encodeURIComponent(file.filePath),
|
||
responseType: 'blob',
|
||
headers: { Authorization: "Bearer " + getToken() }
|
||
}).then(res => {
|
||
const blob = new Blob([res.data])
|
||
saveAs(blob, file.fileName)
|
||
}).catch(() => {
|
||
proxy.$modal.msgError("下载失败")
|
||
}).finally(() => {
|
||
proxy.$modal.closeLoading()
|
||
})
|
||
}
|
||
|
||
/* ================= 初始化(同路由切换客户时需整体重置) ================= */
|
||
function initPage() {
|
||
const customerId = route.query.customerId
|
||
// 重置页面状态,避免上一个客户的数据残留
|
||
customer.value = {}
|
||
accountList.value = []
|
||
activeTab.value = route.query.tab === 'billWriteoff' ? 'billWriteoff' : 'receipt'
|
||
receiptDialog.visible = false
|
||
viewDialog.visible = false
|
||
viewDialog.data = null
|
||
viewDialog.attachments = []
|
||
receiptDateRange.value = []
|
||
flowDateRange.value = []
|
||
creditFlowDateRange.value = []
|
||
creditDialog.visible = false
|
||
receiptQuery.customerId = customerId
|
||
receiptQuery.receiptType = ''
|
||
receiptQuery.status = ''
|
||
receiptQuery.pageNum = 1
|
||
flowQuery.customerId = customerId
|
||
flowQuery.bizType = ''
|
||
flowQuery.direction = ''
|
||
flowQuery.pageNum = 1
|
||
creditFlowQuery.customerId = customerId
|
||
creditFlowQuery.creditType = ''
|
||
creditFlowQuery.changeType = ''
|
||
creditFlowQuery.pageNum = 1
|
||
receiptList.value = []
|
||
receiptTotal.value = 0
|
||
flowList.value = []
|
||
flowTotal.value = 0
|
||
creditFlowList.value = []
|
||
creditFlowTotal.value = 0
|
||
applyList.value = []
|
||
applyTotal.value = 0
|
||
applyQuery.customerId = customerId
|
||
applyQuery.creditType = ''
|
||
applyQuery.status = ''
|
||
applyQuery.pageNum = 1
|
||
creditApplyLoaded = false
|
||
unsettledList.value = []
|
||
writeoffList.value = []
|
||
writeoffTotal.value = 0
|
||
woQuery.customerId = customerId
|
||
woQuery.status = ''
|
||
woQuery.pageNum = 1
|
||
woDialog.visible = false
|
||
cancelWoDialog.visible = false
|
||
billWriteoffLoaded = false
|
||
if (!customerId) return
|
||
loadCustomer()
|
||
loadAccounts()
|
||
getReceiptList()
|
||
getFlowList()
|
||
getCreditFlowList()
|
||
if (activeTab.value === 'billWriteoff') {
|
||
billWriteoffLoaded = true
|
||
loadBillWriteoffData()
|
||
}
|
||
}
|
||
|
||
onMounted(initPage)
|
||
|
||
// 从客户列表点不同客户跳转时,路由路径相同仅 query 变化,组件会被复用,必须监听 customerId 重新初始化
|
||
watch(() => route.query.customerId, (newId, oldId) => {
|
||
if (newId && newId !== oldId) {
|
||
initPage()
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.head-card {
|
||
margin-bottom: 12px;
|
||
}
|
||
.head-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
.head-title {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
.customer-name {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
}
|
||
.head-code {
|
||
color: var(--el-text-color-secondary);
|
||
font-size: 13px;
|
||
}
|
||
.head-right {
|
||
margin-left: auto;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
.balance-row {
|
||
margin-bottom: 12px;
|
||
}
|
||
.balance-card {
|
||
margin-bottom: 12px;
|
||
text-align: center;
|
||
}
|
||
.balance-card {
|
||
min-height: 120px; /* 根据你的实际 UI 调整数值 */
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center; /* 内容垂直居中 */
|
||
}
|
||
|
||
.balance-label {
|
||
color: var(--el-text-color-secondary);
|
||
font-size: 13px;
|
||
margin-bottom: 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 4px;
|
||
}
|
||
.balance-sub {
|
||
margin-top: 6px;
|
||
font-size: 12px;
|
||
color: var(--el-text-color-secondary);
|
||
min-height: 16px;
|
||
}
|
||
.credit-tip {
|
||
margin-bottom: 12px;
|
||
}
|
||
.credit-apply-tip {
|
||
margin-bottom: 12px;
|
||
}
|
||
.credit-preview {
|
||
margin-top: 4px;
|
||
padding: 10px 14px;
|
||
background: var(--el-fill-color-light);
|
||
border-radius: 4px;
|
||
font-size: 13px;
|
||
line-height: 1.8;
|
||
}
|
||
.credit-alert {
|
||
margin-top: 8px;
|
||
margin-bottom: 0;
|
||
}
|
||
.balance-value {
|
||
font-size: 22px;
|
||
font-weight: 700;
|
||
color: var(--el-text-color-primary);
|
||
}
|
||
.balance-value.primary { color: var(--el-color-primary); }
|
||
.balance-value.success { color: var(--el-color-success); }
|
||
.balance-value.warning { color: var(--el-color-warning); }
|
||
.balance-value.danger { color: var(--el-color-danger); }
|
||
.rate-tip {
|
||
margin-top: 4px;
|
||
font-size: 12px;
|
||
color: var(--el-text-color-secondary);
|
||
line-height: 1.4;
|
||
}
|
||
.search-form {
|
||
margin-bottom: -4px;
|
||
}
|
||
.money-in {
|
||
color: var(--el-color-success);
|
||
font-weight: 600;
|
||
}
|
||
.money-out {
|
||
color: var(--el-color-danger);
|
||
font-weight: 600;
|
||
}
|
||
.receipt-upload {
|
||
width: 100%;
|
||
margin-bottom: 8px;
|
||
|
||
:deep(.el-upload),
|
||
:deep(.el-upload-dragger) {
|
||
width: 100%;
|
||
}
|
||
}
|
||
.upload-tip {
|
||
color: #909399;
|
||
font-size: 12px;
|
||
margin-top: 4px;
|
||
}
|
||
.attach-title {
|
||
font-weight: 600;
|
||
margin: 12px 0 8px;
|
||
}
|
||
.writeoff-summary {
|
||
margin-bottom: 12px;
|
||
}
|
||
.receipt-wo-toolbar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14px;
|
||
flex-wrap: wrap;
|
||
margin-bottom: 8px;
|
||
}
|
||
.receipt-wo-summary {
|
||
font-size: 13px;
|
||
color: #606266;
|
||
}
|
||
.wo-summary {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 18px;
|
||
flex-wrap: wrap;
|
||
font-size: 13px;
|
||
}
|
||
.wo-section-title {
|
||
font-weight: 600;
|
||
font-size: 14px;
|
||
margin: 14px 0 8px;
|
||
padding-left: 8px;
|
||
border-left: 3px solid var(--el-color-primary);
|
||
}
|
||
.wo-form {
|
||
margin-top: 14px;
|
||
}
|
||
.wo-form-unit {
|
||
margin-left: 8px;
|
||
color: var(--el-text-color-secondary);
|
||
}
|
||
|
||
|
||
</style>
|