客户余额记录

This commit is contained in:
luojiaquan
2026-09-14 11:51:54 +08:00
parent 4cc20386d9
commit 18f01d1b33
16 changed files with 2349 additions and 43 deletions
@@ -0,0 +1,60 @@
import request from '@/utils/request'
/** 查询客户各币种账户余额 */
export function listAccount(customerId) {
return request({
url: '/customersupplier/customermoney/account/list/' + customerId,
method: 'get'
})
}
/** 查询收款记录列表 */
export function listReceipt(query) {
return request({
url: '/customersupplier/customermoney/receipt/list',
method: 'get',
params: query
})
}
/** 查询收款单详情(含附件) */
export function getReceipt(id) {
return request({
url: '/customersupplier/customermoney/receipt/' + id,
method: 'get'
})
}
/** 登记收款 */
export function addReceipt(data) {
return request({
url: '/customersupplier/customermoney/receipt',
method: 'post',
data: data
})
}
/** 审核通过收款单 */
export function confirmReceipt(id) {
return request({
url: '/customersupplier/customermoney/receipt/confirm/' + id,
method: 'put'
})
}
/** 取消收款单 */
export function cancelReceipt(id) {
return request({
url: '/customersupplier/customermoney/receipt/cancel/' + id,
method: 'put'
})
}
/** 查询余额流水列表 */
export function listFlow(query) {
return request({
url: '/customersupplier/customermoney/flow/list',
method: 'get',
params: query
})
}
+14
View File
@@ -158,6 +158,20 @@ export const dynamicRoutes = [
meta: { title: '修改生成配置', activeMenu: '/tool/gen' }
}
]
},
{
path: '/customersupplier/customer-money',
component: Layout,
hidden: true,
permissions: ['customersupplier:customerinfo:list'],
children: [
{
path: 'index',
component: () => import('@/views/customersuppliermanagement/customerinfo/customermoney/index.vue'),
name: 'CustomerMoney',
meta: { title: '客户余额', activeMenu: '/customersupplier/customerinfo' }
}
]
}
]
@@ -0,0 +1,838 @@
<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">
<span style="color:#909399;font-size:13px;">币种</span>
<el-select v-model="currentCurrency" style="width: 110px;" @change="loadAccounts">
<el-option v-for="acc in accountList" :key="acc.currency" :label="acc.currency" :value="acc.currency" />
</el-select>
</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">账户余额</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">登记收款</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 class="money-in">{{ scope.row.currency }} {{ formatMoney(scope.row.amount) }}</span>
</template>
</el-table-column>
<el-table-column label="待核销" 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)">审核通过</el-button>
<el-button v-if="scope.row.status === 'PENDING'" link type="warning" size="small" @click="handleCancelReceipt(scope.row)">取消</el-button>
<el-button link type="primary" size="small" @click="handleViewReceipt(scope.row)">查看</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">
<span :class="scope.row.direction === 1 ? 'money-in' : 'money-out'">
{{ scope.row.direction === 1 ? '+' : '-' }}{{ formatMoney(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="currency" width="70" />
<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>¥</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%;">
<el-option label="人民币 CNY" value="CNY" />
<el-option label="美元 USD" value="USD" />
<el-option label="欧元 EUR" value="EUR" />
</el-select>
</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="待核销金额">{{ 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"
>审核通过</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 { 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: '解冻' }
]
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)
}
function formatDateTime(val) {
if (!val) return '-'
return String(val).length > 16 ? String(val).substring(0, 16) : val
}
/* ================= 客户信息 & 账户 ================= */
const accountList = ref([])
const currentCurrency = ref('CNY')
const currentAccount = computed(() => {
return accountList.value.find(a => a.currency === currentCurrency.value) || {}
})
/** 可用额度 = 总额度 + 额外额度 - 已用额度 */
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 || []
if (accountList.value.length > 0) {
const exists = accountList.value.some(a => a.currency === currentCurrency.value)
if (!exists) currentCurrency.value = accountList.value[0].currency
}
})
}
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() {
receiptDialog.form = {
customerId: route.query.customerId || null,
receivedAt: proxy.parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}'),
amount: '',
receiptType: 'PREPAY',
currency: 'CNY',
payMethod: 'BANK_TRANSFER',
bankTransactionNo: '',
remark: '',
attachmentList: []
}
proxy.resetForm('receiptFormRef')
}
function handleAddReceipt() {
resetReceiptForm()
receiptDialog.visible = true
}
function submitReceipt() {
receiptFormRef.value.validate(valid => {
if (!valid) 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 = []
currentCurrency.value = 'CNY'
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); }
.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>
@@ -115,7 +115,7 @@
</el-col>
<el-col :span="1.5">
<el-button
type="success"
type="warning"
plain
icon="Edit"
:disabled="single"
@@ -123,6 +123,16 @@
v-hasPermi="['customersupplier:customerinfo:edit']"
>修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="success"
plain
icon="Edit"
:disabled="single"
@click="handleCustomerMoney"
>客户余额</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
@@ -175,8 +185,12 @@
<el-table-column label="客服姓名" align="center" prop="serviceUser" />
<el-table-column label="结算方式" align="center" prop="settlementType" />
<el-table-column label="临时额度" align="center" prop="tempEd" />
<el-table-column label="账户余额" align="center" prop="accountBalance" />
<el-table-column label="外币余额" align="center" prop="foreignCurrencyBalance" />
<el-table-column label="账户余额" align="center" prop="accountBalance" width="110">
<template #default="scope">{{ formatBalance(scope.row.accountBalance) }}</template>
</el-table-column>
<el-table-column label="外币余额" align="center" prop="foreignCurrencyBalance" width="110">
<template #default="scope">{{ formatBalance(scope.row.foreignCurrencyBalance) }}</template>
</el-table-column>
<el-table-column label="重量阈值" align="center" prop="weightThreshold" />
<el-table-column label="抛比" align="center" prop="throwRatio" />
<el-table-column label="信用额度" align="center" prop="creditEd" />
@@ -186,7 +200,7 @@
<template #default="scope">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['customersupplier:customerinfo:edit']">修改</el-button>
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['customersupplier:customerinfo:remove']">删除</el-button>
<el-button link type="primary" icon="Wallet" @click="handleCustomerMoney(scope.row)">客户余额</el-button>
</template>
</el-table-column>
@@ -356,13 +370,17 @@
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="账户余额" prop="accountBalance">
<el-input v-model="form.accountBalance" placeholder="请输入账户余额" />
<el-form-item label="账户余额">
<el-input :model-value="formatBalance(form.accountBalance)" disabled placeholder="0.00">
<template #append>CNY</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="外币余额" prop="foreignCurrencyBalance">
<el-input v-model="form.foreignCurrencyBalance" placeholder="请输入外币余额" />
<el-form-item label="外币余额">
<el-input :model-value="formatBalance(form.foreignCurrencyBalance)" disabled placeholder="0.00">
<template #append>外币</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
@@ -798,6 +816,12 @@ function formatFileSize(bytes) {
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]
}
/** 格式化余额(数据源 customer_account 余额表) */
function formatBalance(val) {
const num = Number(val)
return isNaN(num) ? '0.00' : num.toFixed(2)
}
/** 附件上传前校验 */
function handleBeforeAttachmentUpload(file) {
const ext = (file.name.split('.').pop() || "").toLowerCase()
@@ -959,8 +983,10 @@ function resetQuery() {
}
/** 多选框选中数据 */
const selectedRows = ref([])
function handleSelectionChange(selection) {
ids.value = selection.map(item => item.customerId)
selectedRows.value = selection
single.value = selection.length != 1
multiple.value = !selection.length
}
@@ -996,14 +1022,16 @@ function handleUpdate(row) {
function submitForm() {
proxy.$refs["customerRef"].validate(valid => {
if (valid) {
// 账户余额/外币余额以 customer_account 余额表为准(收款审核自动变动),提交时不写回主表冗余列
const payload = { ...form.value, accountBalance: null, foreignCurrencyBalance: null }
if (form.value.customerId != null) {
updateCustomer(form.value).then(() => {
updateCustomer(payload).then(() => {
proxy.$modal.msgSuccess("修改成功")
open.value = false
getList()
})
} else {
addCustomer(form.value).then(() => {
addCustomer(payload).then(() => {
proxy.$modal.msgSuccess("新增成功")
open.value = false
getList()
@@ -1024,12 +1052,30 @@ function handleDelete(row) {
}).catch(() => {})
}
// /** 导出按钮操作 */
// function handleExport() {
// proxy.download('system/customer/export', {
// ...queryParams.value
// }, `customer_${new Date().getTime()}.xlsx`)
// }
/** 跳转客户余额页面(操作列传 row;工具栏按钮不传参时取勾选行) */
function handleCustomerMoney(row) {
const target = (row && row.customerId) ? row : selectedRows.value[0]
if (!target || !target.customerId) {
proxy.$modal.msgWarning("请先勾选一个客户")
return
}
proxy.$router.push({
path: '/customersupplier/customer-money/index',
query: {
customerId: target.customerId,
customerName: target.customerName,
customerCode: target.customerCode
}
})
}
/** 导出按钮操作 */
function handleExport() {
proxy.download('/customersupplier/customerinfo/export', {
...queryParams.value
}, `customer_${new Date().getTime()}.xlsx`)
}
/* ============ 公司主体子表 ============ */
const companyDialogVisible = ref(false)
@@ -233,6 +233,25 @@
</el-table>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="联系人" prop="contactList">
<div style="margin-bottom: 8px;">
<el-button type="primary" plain icon="Plus" size="small" @click="handleAddContact">新增联系人</el-button>
</div>
<el-table :data="form.contactList" border size="small" :max-height="240">
<el-table-column label="序号" type="index" width="55" align="center" />
<el-table-column label="联系人" align="center" prop="contactName" show-overflow-tooltip />
<el-table-column label="职位" align="center" prop="position" show-overflow-tooltip />
<el-table-column label="手机号" align="center" prop="phone" show-overflow-tooltip />
<el-table-column label="操作" align="center" width="160">
<template #default="scope">
<el-button link type="primary" size="small" @click="handleEditContact(scope.row, scope.$index)">编辑</el-button>
<el-button link type="danger" size="small" @click="handleDeleteContact(scope.$index)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="供应商类型" prop="supplierType">
<el-select v-model="form.supplierType" placeholder="请选择供应商类型" clearable>
@@ -615,6 +634,118 @@
</div>
</template>
</el-dialog>
<!-- 联系人编辑对话框 -->
<el-dialog :title="contactDialogTitle" v-model="contactDialogVisible" width="800px" append-to-body>
<el-form ref="contactRef" :model="contactForm" :rules="contactRules" label-width="120px">
<el-row>
<el-col :span="12">
<el-form-item label="联系人姓名" prop="contactName">
<el-input v-model="contactForm.contactName" placeholder="请输入联系人姓名" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="职位" prop="position">
<el-input v-model="contactForm.position" placeholder="如 销售经理/财务总监" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="手机号" prop="phone">
<el-input v-model="contactForm.phone" placeholder="请输入手机号" maxlength="20" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="所属部门" prop="department">
<el-input v-model="contactForm.department" placeholder="如 销售部/财务部" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="座机" prop="landline">
<el-input v-model="contactForm.landline" placeholder="含区号" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="电子邮箱" prop="email">
<el-input v-model="contactForm.email" placeholder="请输入电子邮箱" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="微信号" prop="wechat">
<el-input v-model="contactForm.wechat" placeholder="请输入微信号" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="QQ号" prop="qq">
<el-input v-model="contactForm.qq" placeholder="请输入QQ号" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="国家/地区" prop="country" label-width="90px">
<el-input v-model="contactForm.country" placeholder="中国" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="省份" prop="province" label-width="60px">
<el-input v-model="contactForm.province" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="城市" prop="city" label-width="60px">
<el-input v-model="contactForm.city" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="区/县" prop="district" label-width="60px">
<el-input v-model="contactForm.district" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="邮政编码" prop="postalCode">
<el-input v-model="contactForm.postalCode" placeholder="请输入邮政编码" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="详细地址" prop="contactAddress">
<el-input v-model="contactForm.contactAddress" placeholder="街道、门牌号" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="联系人类型" prop="contactType">
<el-select v-model="contactForm.contactType" placeholder="请选择" clearable>
<el-option label="业务联系人" value="0" />
<el-option label="财务联系人" value="1" />
<el-option label="紧急联系人" value="2" />
<el-option label="其他" value="3" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="是否默认" prop="isDefault">
<el-radio-group v-model="contactForm.isDefault">
<el-radio label="0">否</el-radio>
<el-radio label="1">是</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="排序" prop="sortOrder">
<el-input-number v-model="contactForm.sortOrder" :min="0" controls-position="right" style="width: 100%;" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input v-model="contactForm.remark" type="textarea" :rows="2" placeholder="请输入备注" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitContactForm">确 定</el-button>
<el-button @click="contactDialogVisible = false">取 消</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
@@ -848,6 +979,7 @@ function reset() {
channelIds: null,
attachmentList: [],
companyList: [],
contactList: [],
remark: null,
createBy: null,
createTime: null,
@@ -896,6 +1028,9 @@ function handleUpdate(row) {
if (!form.value.companyList) {
form.value.companyList = []
}
if (!form.value.contactList) {
form.value.contactList = []
}
open.value = true
title.value = "修改供应商"
})
@@ -1047,6 +1182,86 @@ function submitCompanyForm() {
proxy.$modal.msgSuccess(companyEditingIndex.value >= 0 ? "修改成功" : "新增成功")
}
/* ============ 联系人子表 ============ */
const contactDialogVisible = ref(false)
const contactDialogTitle = ref("")
const contactForm = ref({})
const contactEditingIndex = ref(-1)
const contactRules = {
contactName: [{ required: true, message: '联系人姓名不能为空', trigger: 'blur' }],
phone: [{ required: true, message: '手机号不能为空', trigger: 'blur' }]
}
/** 重置联系人表单 */
function resetContactForm() {
contactForm.value = {
contactId: null,
supplierId: form.value.supplierId || null,
contactName: '',
position: '',
department: '',
phone: '',
landline: '',
email: '',
wechat: '',
qq: '',
country: '中国',
province: '',
city: '',
district: '',
contactAddress: '',
postalCode: '',
isDefault: '0',
contactType: '0',
sortOrder: 0,
remark: ''
}
proxy.resetForm("contactRef")
}
/** 新增联系人(本地子表,跟随主表一起提交) */
function handleAddContact() {
resetContactForm()
contactEditingIndex.value = -1
contactDialogTitle.value = "新增联系人"
contactDialogVisible.value = true
}
/** 编辑联系人 */
function handleEditContact(row, index) {
resetContactForm()
contactForm.value = { ...row }
contactEditingIndex.value = index
contactDialogTitle.value = "修改联系人"
contactDialogVisible.value = true
}
/** 删除联系人(仅从子表移除;未入库的不调后端,已入库的在主表保存后由后端增量处理) */
function handleDeleteContact(index) {
proxy.$modal.confirm('是否确认删除该条联系人记录?').then(() => {
form.value.contactList.splice(index, 1)
proxy.$modal.msgSuccess("删除成功")
}).catch(() => {})
}
/** 提交联系人表单(本地保存,跟随主表提交一起入库) */
function submitContactForm() {
if (!contactForm.value.contactName) {
proxy.$modal.msgWarning("联系人姓名不能为空")
return
}
if (!Array.isArray(form.value.contactList)) {
form.value.contactList = []
}
if (contactEditingIndex.value >= 0) {
form.value.contactList.splice(contactEditingIndex.value, 1, { ...contactForm.value })
} else {
form.value.contactList.push({ ...contactForm.value })
}
contactDialogVisible.value = false
proxy.$modal.msgSuccess(contactEditingIndex.value >= 0 ? "修改成功" : "新增成功")
}
getUserList()
getList()
</script>