Files
BMS/bms-front/src/views/customersuppliermanagement/customerinfo/customermoney/index.vue
T
2026-09-21 17:59:09 +08:00

2263 lines
92 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="goBillView"
v-hasPermi="['ar:bill:list']"
>查看账单</el-button>
<el-button
type="primary"
icon="Money"
@click="openAdjustDialog()"
v-hasPermi="['customer:credit:adjust']"
>额度调整申请</el-button>
</div>
</div>
</el-card>
<!-- 余额概览(数据来源 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 danger">{{ formatMoney(currentAccount.usedCredit) }}</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>
<el-button type="primary" plain size="small" @click="goBillView"
v-hasPermi="['ar:bill:list']">查看全部账单</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" prop="writeoffNo" width="180" />
<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="640px" 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 style="margin-bottom: 8px;">
<el-upload
:show-file-list="false"
:action="uploadFileUrl"
:headers="uploadHeaders"
:before-upload="handleBeforeUpload"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
multiple
>
<el-button type="primary" icon="Upload">上传附件</el-button>
</el-upload>
</div>
<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>
<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.usedCredit) }}</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="creditDialog.form.creditType === 'TEMP' ? '失效日期' : '失效日期'" prop="expireDate">
<el-date-picker
v-model="creditDialog.form.expireDate"
type="date"
value-format="YYYY-MM-DD"
:placeholder="creditDialog.form.creditType === 'TEMP' ? '不填为长期有效' : '必选,到期自动清零'"
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.usedCredit) }})</div>
<el-alert
v-if="creditAfter < 0"
type="error" :closable="false"
title="调整后额度不能为负数" class="credit-alert"
/>
<el-alert
v-else-if="totalCreditAfter < (Number(currentAccount.usedCredit) || 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="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="warning" :closable="false">
<template title>
核销后账户余额变为 CNY {{ woAfterBalance }},同步释放等额已用额度;余额不足时无法提交。
</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">
反核销后,核销金额 CNY {{ formatMoney(cancelWoDialog.row.writeoffAmount) }} 将退回客户账户余额,
账单「{{ cancelWoDialog.row.billNo }}」恢复为未核销,已用额度同步加回。
</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 { 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'
const route = useRoute()
const router = useRouter()
const { proxy } = getCurrentInstance()
const baseUrl = import.meta.env.VITE_APP_BASE_API
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'
}
/**
* 是否为授信占用/释放流水。
* 占用/释放流水的 变动前/变动额/变动后 记的是【已用额度】维度;
* 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 total = Number(a.totalCredit) || 0
const extra = Number(a.extraCredit) || 0
const used = Number(a.usedCredit) || 0
return total + extra - used
})
/** 可用额度展示文本 */
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 (creditDialog.form.creditType === 'CREDIT' && !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.creditType === 'CREDIT' && !f.expireDate) {
proxy.$modal.msgError('请选择信用额度失效日期')
return
}
if (f.expireDate && f.effectiveDate && f.expireDate <= f.effectiveDate) {
proxy.$modal.msgError('失效日期必须晚于生效日期')
return
}
if (creditAfter.value < 0) {
proxy.$modal.msgError('调整后额度不能为负数')
return
}
const used = Number(currentAccount.value.usedCredit) || 0
if (totalCreditAfter.value < used) {
proxy.$modal.msgError('调整后授信总额不能小于已用额度 ' + formatMoney(used))
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()
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: []
}
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 的汇率,请手工录入汇率`)
}
})
}
/** 切换币种:重置手工标记并重新取默认汇率 */
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()
}
})
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
}
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 || []
if (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 = (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)
}
function goBillView() {
router.push({
path: '/ar/bill',
query: { customerId: route.query.customerId || '' }
})
}
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
/** 核销后余额预览(不为负则可提交) */
const woAfterBalance = computed(() => {
const balance = Number(currentAccount.value.balance) || 0
const amount = Number(woDialog.form.writeoffAmount) || 0
return formatMoney(balance - amount)
})
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,
// 余额接口返回前先显示 0,避免使用旧余额或在余额为 0 时错误带出整笔未核销金额。
writeoffAmount: 0,
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 || []
const balance = Math.max(0, Number(currentAccount.value.balance) || 0)
woDialog.form.writeoffAmount = Math.min(unwritten, balance)
} catch (e) {
if (requestSerial !== writeoffAccountRequestSerial) return
woDialog.form.writeoffAmount = 0
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
}
const balance = Number(currentAccount.value.balance) || 0
if (amount > balance) {
proxy.$modal.msgError('账户余额不足,当前余额 CNY ' + formatMoney(balance) + ',请先登记收款并审核')
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")
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;
}
.upload-tip {
color: #909399;
font-size: 12px;
margin-top: 4px;
}
.attach-title {
font-weight: 600;
margin: 12px 0 8px;
}
.writeoff-summary {
margin-bottom: 12px;
}
.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>