Files
BMS/bms-front/src/views/customersuppliermanagement/customerinfo/customermoney/index.vue
T
2026-10-27 20:29:08 +08:00

2525 lines
104 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<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>