Files
BMS/bms-front/src/views/customersuppliermanagement/customerinfo/customermoney/index.vue
T
2026-09-15 16:34:24 +08:00

951 lines
37 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>
</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>