951 lines
37 KiB
Vue
951 lines
37 KiB
Vue
<template>
|
||
<div class="app-container">
|
||
<!-- 顶部:返回 + 标题 -->
|
||
<el-card shadow="never" class="head-card">
|
||
<div class="head-bar">
|
||
<el-button icon="ArrowLeft" @click="goBack">返回</el-button>
|
||
<div class="head-title">
|
||
<span class="customer-name">{{ customer.customerName || route.query.customerName || '-' }}</span>
|
||
<el-tag size="small" type="info">客户余额</el-tag>
|
||
</div>
|
||
<div class="head-code">客户编码:{{ customer.customerCode || route.query.customerCode || '-' }}</div>
|
||
<div class="head-right">
|
||
<el-tag type="success" size="small">本位币账户 CNY</el-tag>
|
||
</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">总额度</div>
|
||
<div class="balance-value">{{ formatMoney(currentAccount.totalCredit) }}</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">{{ formatMoney(currentAccount.extraCredit) }}</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 success">{{ availableCredit }}</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-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="110">
|
||
<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="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" 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-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-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<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="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="本位币金额">
|
||
<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="银行流水号">{{ 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" />
|
||
<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>
|
||
</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 } from "@/api/customersuppliermanagement/customermoney/customermoney.js"
|
||
import { convertToBase } from "@/api/data/exchangeRate"
|
||
import { getToken } from "@/utils/auth"
|
||
|
||
import { useRoute } from 'vue-router'
|
||
|
||
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: 'BILL_PAY', 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'
|
||
}
|
||
function bizTypeTag(v) {
|
||
return { RECEIPT: 'success', BILL_PAY: 'danger', REFUND: 'warning', ADJUST: 'info', FREEZE: 'info', UNFREEZE: '' }[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 availableCredit = computed(() => {
|
||
const a = currentAccount.value
|
||
const total = Number(a.totalCredit) || 0
|
||
const extra = Number(a.extraCredit) || 0
|
||
const used = Number(a.usedCredit) || 0
|
||
return formatMoney(total + extra - used)
|
||
})
|
||
|
||
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 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
|
||
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',
|
||
bankTransactionNo: '',
|
||
remark: '',
|
||
attachmentList: []
|
||
}
|
||
proxy.resetForm('receiptFormRef')
|
||
}
|
||
|
||
function handleAddReceipt() {
|
||
resetReceiptForm()
|
||
receiptDialog.visible = true
|
||
}
|
||
|
||
/** 汇率是否被手工修改过(修改后切换币种/日期不再自动覆盖) */
|
||
const rateEdited = ref(false)
|
||
|
||
/** 按收款日期从汇率表取该币种对CNY即期汇率 */
|
||
function fetchDefaultRate() {
|
||
const cur = receiptDialog.form.currency
|
||
if (!cur || cur === 'CNY') {
|
||
receiptDialog.form.exchangeRate = 1
|
||
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)
|
||
})
|
||
.catch(() => {
|
||
if (!rateEdited.value) {
|
||
receiptDialog.form.exchangeRate = undefined
|
||
proxy.$modal.msgWarning(`汇率表中未维护 ${cur} 对 CNY 的汇率,请手工录入汇率`)
|
||
}
|
||
})
|
||
}
|
||
|
||
/** 切换币种:重置手工标记并重新取默认汇率 */
|
||
function handleCurrencyChange() {
|
||
rateEdited.value = false
|
||
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 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,
|
||
bizId: null,
|
||
fileName: file.name,
|
||
filePath: res.fileName,
|
||
fileSize: file.size,
|
||
fileType: file.type || '',
|
||
fileExtension: (file.name.split('.').pop() || "").toLowerCase(),
|
||
attachmentType: "3",
|
||
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 = 'receipt'
|
||
receiptDialog.visible = false
|
||
viewDialog.visible = false
|
||
viewDialog.data = null
|
||
viewDialog.attachments = []
|
||
receiptDateRange.value = []
|
||
flowDateRange.value = []
|
||
receiptQuery.customerId = customerId
|
||
receiptQuery.receiptType = ''
|
||
receiptQuery.status = ''
|
||
receiptQuery.pageNum = 1
|
||
flowQuery.customerId = customerId
|
||
flowQuery.bizType = ''
|
||
flowQuery.direction = ''
|
||
flowQuery.pageNum = 1
|
||
receiptList.value = []
|
||
receiptTotal.value = 0
|
||
flowList.value = []
|
||
flowTotal.value = 0
|
||
if (!customerId) return
|
||
loadCustomer()
|
||
loadAccounts()
|
||
getReceiptList()
|
||
getFlowList()
|
||
}
|
||
|
||
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-label {
|
||
color: var(--el-text-color-secondary);
|
||
font-size: 13px;
|
||
margin-bottom: 8px;
|
||
}
|
||
.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;
|
||
}
|
||
</style>
|