This commit is contained in:
wuweihua
2026-09-16 17:14:24 +08:00
45 changed files with 3786 additions and 77 deletions
@@ -0,0 +1,36 @@
import request from '@/utils/request.js'
// 查询客户已绑定的套餐列表
export function listCustomerPriceSet(customerId) {
return request({
url: '/customersupplier/customerinfo/priceset/bind/list',
method: 'get',
params: { customerId }
})
}
// 查询可绑定的生效套餐选项
export function listPriceSetOption(query) {
return request({
url: '/customersupplier/customerinfo/priceset/option/list',
method: 'get',
params: query
})
}
// 保存客户套餐绑定(已绑定则更新/更换,未绑定则新增)
export function saveCustomerPriceSet(data) {
return request({
url: '/customersupplier/customerinfo/priceset/bind',
method: 'post',
data
})
}
// 解绑客户套餐
export function delCustomerPriceSet(customerPricesetIds) {
return request({
url: '/customersupplier/customerinfo/priceset/bind/' + customerPricesetIds,
method: 'delete'
})
}
@@ -0,0 +1,53 @@
import request from '@/utils/request'
/** 查询客户可核销账单(已确认/部分核销) */
export function listUnsettledBill(customerId) {
return request({
url: '/customersupplier/customerbill/unsettled/' + customerId,
method: 'get'
})
}
/** 查询余额核销记录 */
export function listBalanceWriteoff(query) {
return request({
url: '/customersupplier/customerbill/writeoff/list',
method: 'get',
params: query
})
}
/** 用客户账户余额核销账单 */
export function balanceWriteoff(data) {
return request({
url: '/customersupplier/customerbill/writeoff',
method: 'post',
data: data
})
}
/** 反核销(取消一笔有效余额核销) */
export function cancelBalanceWriteoff(writeoffId, cancelRemark) {
return request({
url: '/customersupplier/customerbill/writeoff/' + writeoffId + '/cancel',
method: 'put',
data: { cancelRemark }
})
}
/** 账单查看:账单列表(只读) */
export function listCustomerBill(query) {
return request({
url: '/customersupplier/customerbill/list',
method: 'get',
params: query
})
}
/** 账单查看:账单详情(含明细) */
export function getCustomerBill(arBillId) {
return request({
url: '/customersupplier/customerbill/' + arBillId,
method: 'get'
})
}
@@ -58,3 +58,21 @@ export function listFlow(query) {
params: query
})
}
/** 查询额度流水列表 */
export function listCreditFlow(query) {
return request({
url: '/customersupplier/customermoney/credit/flow/list',
method: 'get',
params: query
})
}
/** 调整客户额度(信用额度/临时额度),调整过程留额度流水 */
export function adjustCredit(data) {
return request({
url: '/customersupplier/customermoney/credit/adjust',
method: 'post',
data: data
})
}
+62 -5
View File
@@ -1,9 +1,12 @@
<template>
<div class="app-container ar-bill-page">
<basic-table ref="tableRef" :columns="columns" :search-columns="searchColumns" :query="queryList" row-key="arBillId" :selection="false">
<basic-table ref="tableRef" :columns="columns" :search-columns="searchColumns" :search-label-width="82" :query="queryList" row-key="arBillId" :selection="false">
<template #toolbar>
<el-col :span="1.5"><el-button type="primary" plain icon="Plus" v-hasPermi="['ar:bill:generate']" @click="generateOpen = true">生成账单</el-button></el-col>
</template>
<template #search-customerId="{ model }">
<customer-select v-model="model.customerId" placeholder="请选择客户" :page-size="1000" />
</template>
<template #col-status="{ row }"><el-tag :type="statusType(row.status)">{{ statusText(row.status) }}</el-tag></template>
<template #col-totalAmount="{ row }"><span class="amount">{{ money(row.totalAmount) }}</span></template>
<template #col-writtenOffAmount="{ row }"><span class="written">{{ money(row.writtenOffAmount) }}</span></template>
@@ -36,18 +39,18 @@ const detail = ref(null)
const generateOpen = ref(false)
const statusOptions = AR_BILL_STATUS_OPTIONS
const columns = [
{ label: '账单号', prop: 'billNo', minWidth: 180, showOverflowTooltip: true }, { label: '账单名称', prop: 'billName', minWidth: 180, showOverflowTooltip: true }, { label: '客户', prop: 'customerName', minWidth: 130, showOverflowTooltip: true },
{ label: '账单号', prop: 'billNo', minWidth: 180, showOverflowTooltip: true }, { label: '账单名称', prop: 'billName', minWidth: 180, showOverflowTooltip: true }, { label: '客户名称', prop: 'customerName', minWidth: 130, showOverflowTooltip: true }, { label: '客户编码', prop: 'customerCode', minWidth: 120, showOverflowTooltip: true },
{ label: '账单截止日', prop: 'billPeriodEnd', minWidth: 120, formatter: row => `截至 ${date(row.billPeriodEnd)}` }, { label: '订单数', prop: 'orderCount', width: 80 },
{ label: '应收总额', prop: 'totalAmount', width: 120, align: 'right', slot: 'totalAmount' }, { label: '已核销', prop: 'writtenOffAmount', width: 110, align: 'right', slot: 'writtenOffAmount' },
{ label: '未核销', prop: 'unwrittenAmount', width: 110, align: 'right', slot: 'unwrittenAmount' }, { label: '状态', prop: 'status', width: 110, slot: 'status' },
{ label: '账单日期', prop: 'billDate', width: 120, formatter: row => date(row.billDate) }, { label: '操作', prop: 'action', width: 260, fixed: 'right', slot: 'action', showInToolbar: false }
]
const searchColumns = [
{ label: '账单号', prop: 'billNo', component: 'input', placeholder: '账单号', width: '180px' }, { label: '账单名称', prop: 'billName', component: 'input', placeholder: '账单名称', width: '180px' }, { label: '客户编码', prop: 'customerCode', component: 'input', placeholder: '客户编码', width: '140px' },
{ label: '状态', prop: 'status', component: 'select', options: statusOptions, placeholder: '全部状态', width: '150px' }, { label: '账期', prop: 'periodRange', component: 'daterange', width: '300px' }
{ label: '账单号', prop: 'billNo', component: 'input', placeholder: '账单号', width: '180px' }, { label: '账单名称', prop: 'billName', component: 'input', placeholder: '账单名称', width: '180px' }, { label: '客户名称', prop: 'customerId', component: 'input', placeholder: '请选择客户', width: '220px' }, { label: '客户编码', prop: 'customerCode', component: 'input', placeholder: '客户编码', width: '140px' },
{ label: '状态', prop: 'status', component: 'select', options: statusOptions, placeholder: '全部状态', width: '150px' }, { label: '账单日期', prop: 'billDateRange', component: 'daterange', width: '300px' }, { label: '账单截止日', prop: 'billPeriodEnd', component: 'date', width: '160px' }
]
function queryList(params) { const query = { ...params }; if (query.periodRange?.length === 2) { query.billPeriodStart = query.periodRange[0]; query.billPeriodEnd = query.periodRange[1] }; delete query.periodRange; return listArBill(query) }
function queryList(params) { const query = { ...params }; if (query.billDateRange?.length === 2) { query.billDateStart = query.billDateRange[0]; query.billDateEnd = query.billDateRange[1] }; delete query.billDateRange; return listArBill(query) }
function handleGenerateSuccess(bill) { tableRef.value.refresh(); viewBill(bill) }
function viewBill(row) { if (!row?.arBillId) return; detailOpen.value = true; detailLoading.value = true; detail.value = null; getArBill(row.arBillId).then(res => { detail.value = res.data || null }).finally(() => { detailLoading.value = false }) }
function confirmBill(row) { proxy.$modal.confirm(`确认账单「${row.billNo}」?确认后将进入正式账单状态,请确认订单及费用金额无误后继续。`).then(() => confirmArBill(row.arBillId)).then(() => { proxy.$modal.msgSuccess('账单已确认'); tableRef.value.refresh(); viewBill(row) }).catch(() => {}) }
@@ -61,7 +64,61 @@ function statusType(value) { return AR_BILL_STATUS_TYPES[value] || 'info' }
</script>
<style scoped>
.ar-bill-page :deep(.basic-table > .basic-form) {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px 20px;
align-items: center;
padding: 16px 18px 4px;
margin-bottom: 18px;
border: 1px solid var(--el-border-color-lighter);
border-radius: 8px;
background: var(--el-bg-color);
}
.ar-bill-page :deep(.basic-table > .basic-form > .el-form-item) {
min-width: 0;
margin-right: 0;
margin-bottom: 0;
}
.ar-bill-page :deep(.basic-table > .basic-form > .el-form-item:not(:last-child) .el-form-item__content) {
min-width: 0;
}
.ar-bill-page :deep(.basic-table > .basic-form > .el-form-item:not(:last-child) .el-form-item__content > *) {
width: 100% !important;
}
.ar-bill-page :deep(.basic-table > .basic-form > .el-form-item:last-child) {
grid-column: 1 / -1;
justify-content: flex-start;
}
.ar-bill-page :deep(.basic-table > .basic-form > .el-form-item:last-child .el-form-item__content) {
justify-content: flex-start;
}
.ar-bill-page :deep(.basic-table > .basic-form > .el-form-item:last-child .el-button) {
min-width: 88px;
margin-left: 0;
margin-right: 12px;
}
.ar-bill-page .amount { color: #f56c6c; font-weight: 600; }
.ar-bill-page .written { color: #67c23a; }
.ar-bill-page .unwritten { color: #e6a23c; }
@media (max-width: 1200px) {
.ar-bill-page :deep(.basic-table > .basic-form) {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 768px) {
.ar-bill-page :deep(.basic-table > .basic-form) {
grid-template-columns: minmax(0, 1fr);
padding: 14px 12px 4px;
}
}
</style>
@@ -0,0 +1,301 @@
<template>
<div class="app-container">
<!-- 查询条件 -->
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch">
<el-form-item label="账单号" prop="billNo">
<el-input v-model="queryParams.billNo" placeholder="请输入账单号" clearable style="width: 180px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="账单名称" prop="billName">
<el-input v-model="queryParams.billName" placeholder="请输入账单名称" clearable style="width: 180px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="客户名称" prop="customerName">
<el-input v-model="queryParams.customerName" placeholder="请输入客户名称" clearable style="width: 180px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="全部状态" clearable style="width: 140px">
<el-option v-for="opt in statusOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
</el-form-item>
<el-form-item label="币种" prop="currency">
<el-select v-model="queryParams.currency" placeholder="全部币种" clearable style="width: 120px">
<el-option v-for="c in currencyOptions" :key="c" :label="c" :value="c" />
</el-select>
</el-form-item>
<el-form-item label="账期">
<el-date-picker
v-model="dateRange"
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="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button icon="Search" @click="toggleSearch">{{ showSearch ? '收起' : '展开' }}</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" />
</el-row>
<el-table v-loading="loading" :data="billList" border>
<el-table-column label="账单号" align="center" prop="billNo" min-width="170" show-overflow-tooltip />
<el-table-column label="账单名称" align="center" prop="billName" min-width="150" show-overflow-tooltip />
<el-table-column label="客户" align="center" prop="customerName" min-width="150" show-overflow-tooltip />
<el-table-column label="账期开始" align="center" prop="billPeriodStart" width="110">
<template #default="scope">{{ fmtDate(scope.row.billPeriodStart) }}</template>
</el-table-column>
<el-table-column label="账期结束" align="center" prop="billPeriodEnd" width="110">
<template #default="scope">{{ fmtDate(scope.row.billPeriodEnd) }}</template>
</el-table-column>
<el-table-column label="账单日期" align="center" prop="billDate" width="110">
<template #default="scope">{{ fmtDate(scope.row.billDate) }}</template>
</el-table-column>
<el-table-column label="订单数" align="center" prop="orderCount" width="80" />
<el-table-column label="应收金额" align="right" width="130">
<template #default="scope">
<span class="amount">{{ scope.row.currency }} {{ fmtMoney(scope.row.totalAmount) }}</span>
</template>
</el-table-column>
<el-table-column label="已核销" align="right" width="120">
<template #default="scope"><span class="written">{{ fmtMoney(scope.row.writtenOffAmount) }}</span></template>
</el-table-column>
<el-table-column label="未核销" align="right" width="120">
<template #default="scope"><span class="unwritten">{{ fmtMoney(scope.row.unwrittenAmount) }}</span></template>
</el-table-column>
<el-table-column label="状态" align="center" width="100">
<template #default="scope">
<el-tag :type="statusTag(scope.row.status)">{{ statusLabel(scope.row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="80" fixed="right">
<template #default="scope">
<el-button link type="primary" icon="View" @click="handleView(scope.row)"
v-hasPermi="['customer:bill:query']">详情</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total > 0"
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
<!-- 账单详情抽屉(只读) -->
<el-drawer v-model="detailOpen" size="72%" :title="drawerTitle" destroy-on-close>
<div v-loading="detailLoading" class="bill-detail">
<template v-if="detail">
<el-descriptions :column="3" border size="small">
<el-descriptions-item label="账单号">{{ detail.billNo }}</el-descriptions-item>
<el-descriptions-item label="账单名称" :span="2">{{ detail.billName || '-' }}</el-descriptions-item>
<el-descriptions-item label="客户">{{ detail.customerName }}</el-descriptions-item>
<el-descriptions-item label="客户编码">{{ detail.customerCode || '-' }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="statusTag(detail.status)">{{ statusLabel(detail.status) }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="账期开始">{{ fmtDate(detail.billPeriodStart) }}</el-descriptions-item>
<el-descriptions-item label="账期结束">{{ fmtDate(detail.billPeriodEnd) }}</el-descriptions-item>
<el-descriptions-item label="账单日期">{{ fmtDate(detail.billDate) }}</el-descriptions-item>
<el-descriptions-item label="币种">{{ detail.currency }}</el-descriptions-item>
<el-descriptions-item label="订单数">{{ detail.orderCount }}</el-descriptions-item>
<el-descriptions-item label="应收金额">
<span class="amount">{{ fmtMoney(detail.totalAmount) }}</span>
</el-descriptions-item>
<el-descriptions-item label="已核销金额">
<span class="written">{{ fmtMoney(detail.writtenOffAmount) }}</span>
</el-descriptions-item>
<el-descriptions-item label="未核销金额">
<span class="unwritten">{{ fmtMoney(detail.unwrittenAmount) }}</span>
</el-descriptions-item>
<el-descriptions-item label="确认人">{{ detail.confirmedBy || '-' }}</el-descriptions-item>
<el-descriptions-item label="确认时间">{{ fmtDateTime(detail.confirmedTime) }}</el-descriptions-item>
<el-descriptions-item label="收款银行">
{{ [detail.bankName, detail.bankAccountName].filter(Boolean).join(' / ') || '-' }}
</el-descriptions-item>
<el-descriptions-item label="备注" :span="3">{{ detail.remark || '-' }}</el-descriptions-item>
</el-descriptions>
<div class="detail-title">订单明细({{ detail.details?.length || 0 }} 单)</div>
<el-table :data="detail.details || []" border size="small" row-key="detailId">
<el-table-column type="expand">
<template #default="props">
<div class="fee-wrap">
<div class="fee-title">费用明细</div>
<el-table :data="props.row.fees || []" border size="small">
<el-table-column label="序号" type="index" width="55" align="center" />
<el-table-column label="费用名称" prop="feeName" min-width="120" show-overflow-tooltip />
<el-table-column label="数量" prop="quantity" width="90" align="right" />
<el-table-column label="单位" prop="unit" width="70" align="center" />
<el-table-column label="单价" prop="unitPrice" width="100" align="right" />
<el-table-column label="金额" prop="amount" width="110" align="right">
<template #default="f">{{ f.row.currency }} {{ fmtMoney(f.row.amount) }}</template>
</el-table-column>
<el-table-column label="备注" prop="remark" min-width="120" show-overflow-tooltip />
<template #empty><span class="empty-tip">无费用明细</span></template>
</el-table>
</div>
</template>
</el-table-column>
<el-table-column label="序号" prop="detailNo" width="60" align="center" />
<el-table-column label="订单号" prop="orderNo" min-width="140" show-overflow-tooltip />
<el-table-column label="跟踪号" prop="trackingNo" min-width="140" show-overflow-tooltip />
<el-table-column label="产品" prop="productName" min-width="120" show-overflow-tooltip />
<el-table-column label="目的国" prop="countryName" width="100" align="center" />
<el-table-column label="实重" prop="actualWeight" width="80" align="right" />
<el-table-column label="体积重" prop="volumeWeight" width="80" align="right" />
<el-table-column label="计费重" prop="chargeWeight" width="80" align="right" />
<el-table-column label="金额" prop="totalAmount" width="120" align="right">
<template #default="scope">{{ scope.row.currency }} {{ fmtMoney(scope.row.totalAmount) }}</template>
</el-table-column>
</el-table>
</template>
</div>
</el-drawer>
</div>
</template>
<script setup name="CustomerBill">
import { listCustomerBill, getCustomerBill } from '@/api/customersuppliermanagement/customermoney/customerbill'
const { proxy } = getCurrentInstance()
const route = useRoute()
const statusOptions = [
{ value: 'DRAFT', label: '草稿' },
{ value: 'CONFIRMED', label: '已确认' },
{ value: 'PARTIAL_WRITTEN_OFF', label: '部分核销' },
{ value: 'WRITTEN_OFF', label: '已核销' },
{ value: 'VOID', label: '已作废' }
]
const statusTagMap = { DRAFT: 'info', CONFIRMED: 'warning', PARTIAL_WRITTEN_OFF: 'primary', WRITTEN_OFF: 'success', VOID: 'danger' }
const currencyOptions = ['CNY', 'USD', 'EUR', 'GBP', 'HKD', 'JPY']
const billList = ref([])
const loading = ref(false)
const total = ref(0)
const showSearch = ref(true)
const dateRange = ref([])
const queryParams = reactive({
pageNum: 1,
pageSize: 10,
billNo: '',
billName: '',
customerName: '',
customerId: route.query.customerId || undefined,
status: '',
currency: ''
})
const detailOpen = ref(false)
const detailLoading = ref(false)
const detail = ref(null)
const drawerTitle = computed(() => detail.value ? `账单详情 - ${detail.value.billNo}` : '账单详情')
function statusLabel(v) {
return statusOptions.find(o => o.value === v)?.label || v || '-'
}
function statusTag(v) {
return statusTagMap[v] || 'info'
}
function fmtMoney(v) {
const n = Number(v)
return isNaN(n) ? '0.00' : n.toFixed(2)
}
function fmtDate(v) {
return v ? String(v).substring(0, 10) : '-'
}
function fmtDateTime(v) {
return v ? String(v).substring(0, 16) : '-'
}
function getList() {
loading.value = true
const params = proxy.addDateRange({ ...queryParams }, dateRange.value)
listCustomerBill(params).then(res => {
billList.value = res.rows || []
total.value = res.total || 0
}).finally(() => {
loading.value = false
})
}
function handleQuery() {
queryParams.pageNum = 1
getList()
}
function resetQuery() {
dateRange.value = []
proxy.resetForm('queryRef')
// 从客户余额页跳入时保留客户过滤
queryParams.customerId = route.query.customerId || undefined
handleQuery()
}
function toggleSearch() {
showSearch.value = !showSearch.value
}
function handleView(row) {
detail.value = null
detailOpen.value = true
detailLoading.value = true
getCustomerBill(row.arBillId).then(res => {
detail.value = res.data || null
}).finally(() => {
detailLoading.value = false
})
}
getList()
</script>
<style scoped>
.amount {
color: var(--el-color-danger);
font-weight: 600;
}
.written {
color: var(--el-color-success);
font-weight: 600;
}
.unwritten {
color: var(--el-color-warning);
font-weight: 600;
}
.bill-detail {
padding: 0 16px 16px;
}
.detail-title {
font-weight: 600;
font-size: 14px;
margin: 16px 0 8px;
padding-left: 8px;
border-left: 3px solid var(--el-color-primary);
}
.fee-wrap {
padding: 8px 16px;
background: var(--el-fill-color-light);
}
.fee-title {
font-weight: 600;
margin-bottom: 8px;
font-size: 13px;
}
.empty-tip {
color: var(--el-text-color-secondary);
font-size: 12px;
}
.mb8 {
margin-bottom: 8px;
}
</style>
@@ -108,7 +108,11 @@
</el-col>
<el-col :span="12">
<el-form-item label="临时额度" prop="tempEd">
<el-input v-model="form.tempEd" placeholder="请输入临时额度" />
<el-input v-model="form.tempEd" :disabled="!isAdd" placeholder="请输入临时额度">
<template #append v-if="!isAdd">
<span style="color: #e6a23c">请在客户余额-额度管理中调整</span>
</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="12">
@@ -130,7 +134,11 @@
</el-col>
<el-col :span="12">
<el-form-item label="信用额度" prop="creditEd">
<el-input v-model="form.creditEd" placeholder="请输入信用额度" />
<el-input v-model="form.creditEd" :disabled="!isAdd" placeholder="请输入信用额度">
<template #append v-if="!isAdd">
<span style="color: #e6a23c">请在客户余额-额度管理中调整</span>
</template>
</el-input>
</el-form-item>
</el-col>
<el-col :span="24">
@@ -178,6 +186,50 @@
</el-table>
</el-tab-pane>
<!-- ============ 绑定套餐 ============ -->
<el-tab-pane label="绑定套餐" name="priceSet">
<div class="pane-toolbar">
<el-button type="primary" plain icon="Plus" size="small" :disabled="isAdd" @click="openPriceSetDialog()">
{{ boundPriceSets.length ? '更换套餐' : '绑定套餐' }}
</el-button>
<span class="pane-tip">绑定后写入客户套餐关系表 prc_customer_price_set,一个客户仅可绑定一个价格套餐,仅「生效中」的套餐可绑定</span>
</div>
<el-table :data="boundPriceSets" border size="small" class="bound-table">
<el-table-column label="序号" type="index" width="55" align="center" />
<el-table-column label="套餐编码" prop="priceSetCode" width="120" show-overflow-tooltip />
<el-table-column label="套餐名称" prop="priceSetName" min-width="180" show-overflow-tooltip />
<el-table-column label="计价类型" width="90" align="center">
<template #default="{ row }">
<el-tag :type="row.setType === 'DISCOUNT' ? 'warning' : 'primary'" size="small">
{{ setTypeMap[row.setType] || row.setType || '-' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="套餐状态" width="90" align="center">
<template #default="{ row }">
<el-tag :type="priceSetStatusTagType[row.priceSetStatus] || 'info'" size="small">
{{ priceSetStatusMap[row.priceSetStatus] || '未知' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="优先级" prop="bindPriority" width="70" align="center" />
<el-table-column label="绑定生效" prop="prBegindate" width="110" align="center" />
<el-table-column label="绑定失效" width="100" align="center">
<template #default="{ row }">{{ row.prEnddate || '长期' }}</template>
</el-table-column>
<el-table-column label="备注" prop="priceSetNote" min-width="140" show-overflow-tooltip />
<el-table-column label="操作" width="120" align="center">
<template #default="{ row }">
<el-button link type="primary" size="small" @click="openPriceSetDialog(row)">更换</el-button>
<el-button link type="danger" size="small" @click="handleUnbindPriceSet(row)">解绑</el-button>
</template>
</el-table-column>
<template #empty>
<el-empty :description="isAdd ? '请先保存客户后再绑定套餐' : '暂未绑定套餐,点击上方「绑定套餐」'" :image-size="80" />
</template>
</el-table>
</el-tab-pane>
<!-- ============ 公司主体 ============ -->
<el-tab-pane label="公司主体" name="company">
<div class="pane-toolbar">
@@ -574,6 +626,75 @@
</div>
</template>
</el-dialog>
<!-- ====== 绑定套餐对话框 ====== -->
<el-dialog :title="priceSetDialogTitle" v-model="priceSetDialogVisible" width="560px" append-to-body>
<el-form ref="priceSetRef" :model="priceSetForm" :rules="priceSetRules" label-width="96px">
<el-form-item label="价格套餐" prop="priceSetId">
<el-select
v-model="priceSetForm.priceSetId"
filterable
placeholder="请选择生效中的套餐"
style="width: 100%;"
@change="onPriceSetChange"
>
<el-option
v-for="opt in priceSetOptions"
:key="opt.priceSetId"
:label="((opt.priceSetCode || '') + ' ' + opt.priceSetName).trim()"
:value="opt.priceSetId"
/>
</el-select>
</el-form-item>
<el-form-item v-if="selectedPriceSet" label="套餐信息">
<div class="ps-readonly">
<el-tag size="small" :type="selectedPriceSet.setType === 'DISCOUNT' ? 'warning' : 'primary'">
{{ setTypeMap[selectedPriceSet.setType] || selectedPriceSet.setType }}
</el-tag>
<span class="ps-readonly__range">
套餐有效期:{{ selectedPriceSet.effectiveDate || '-' }} 至 {{ selectedPriceSet.expireDate || '长期' }}
</span>
</div>
</el-form-item>
<el-form-item label="优先级" prop="bindPriority">
<el-input-number v-model="priceSetForm.bindPriority" :min="1" controls-position="right" style="width: 100%;" />
<div class="pane-tip">数字越小优先级越高,1 为最高</div>
</el-form-item>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="生效日期" prop="prBegindate">
<el-date-picker
v-model="priceSetForm.prBegindate"
type="date"
value-format="YYYY-MM-DD"
placeholder="选择日期"
style="width: 100%;"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="失效日期" prop="prEnddate">
<el-date-picker
v-model="priceSetForm.prEnddate"
type="date"
value-format="YYYY-MM-DD"
placeholder="长期有效"
style="width: 100%;"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="备注" prop="priceSetNote">
<el-input v-model="priceSetForm.priceSetNote" type="textarea" :rows="2" maxlength="100" show-word-limit placeholder="绑定备注(选填)" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" :loading="priceSetSaving" @click="submitPriceSetForm">确 定</el-button>
<el-button @click="priceSetDialogVisible = false">取 消</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
@@ -582,6 +703,10 @@ import {
listCustomer, getCustomer, addCustomer, updateCustomer,
delCustomerCompany, delCustomerContact
} from '@/api/customersuppliermanagement/customerinfo/customer.js'
import {
listCustomerPriceSet, listPriceSetOption,
saveCustomerPriceSet, delCustomerPriceSet
} from '@/api/customersuppliermanagement/customerinfo/customerPriceSet.js'
import { getToken } from '@/utils/auth'
import axios from 'axios'
import { saveAs } from 'file-saver'
@@ -630,7 +755,7 @@ function buildEmptyForm() {
foreignCurrencyBalance: null,
weightThreshold: null,
throwRatio: null,
creditEd: null,
creditEd: 1000,
productIds: null,
attachmentList: [],
companyList: [],
@@ -656,6 +781,99 @@ function removeProduct(productId) {
form.value.productIds = ids.join(',')
}
/* ---------------- 套餐绑定(prc_customer_price_set,一客户一套餐) ---------------- */
const setTypeMap = { FIXED: '固定价', DISCOUNT: '折扣' }
const priceSetStatusMap = { D: '草稿', A: '审核中', E: '生效', R: '驳回', S: '停用' }
const priceSetStatusTagType = { D: 'info', A: 'warning', E: 'success', R: 'danger', S: 'info' }
const boundPriceSets = ref([])
const priceSetOptions = ref([])
const priceSetDialogVisible = ref(false)
const priceSetDialogTitle = ref('')
const priceSetSaving = ref(false)
const priceSetRef = ref()
const priceSetForm = ref({})
const priceSetRules = {
priceSetId: [{ required: true, message: '请选择价格套餐', trigger: 'change' }],
bindPriority: [{ required: true, message: '优先级不能为空', trigger: 'blur' }],
prBegindate: [{ required: true, message: '绑定生效日期不能为空', trigger: 'change' }]
}
const selectedPriceSet = computed(() =>
priceSetOptions.value.find(o => o.priceSetId === priceSetForm.value.priceSetId)
)
/** 加载当前客户已绑定套餐 */
function loadBoundPriceSets() {
if (!form.value.customerId) {
boundPriceSets.value = []
return
}
listCustomerPriceSet(form.value.customerId).then(res => {
boundPriceSets.value = res.data || []
})
}
/** 加载可绑定的生效套餐(仅加载一次) */
function ensurePriceSetOptions() {
if (priceSetOptions.value.length) return
listPriceSetOption().then(res => {
priceSetOptions.value = res.data || []
})
}
/** 打开绑定/更换套餐弹窗:row 为空=新增绑定,有值=更换 */
function openPriceSetDialog(row) {
if (!form.value.customerId) {
proxy.$modal.msgWarning('请先保存客户后再绑定套餐')
return
}
ensurePriceSetOptions()
priceSetForm.value = row
? { ...row }
: { priceSetId: null, bindPriority: 1, prBegindate: null, prEnddate: null, priceSetNote: null }
priceSetDialogTitle.value = row || boundPriceSets.value.length ? '更换套餐' : '绑定套餐'
priceSetDialogVisible.value = true
nextTick(() => proxy.$refs['priceSetRef'] && proxy.$refs['priceSetRef'].clearValidate())
}
/** 选择套餐后,生效日期未填时默认带出套餐生效日期 */
function onPriceSetChange(priceSetId) {
const opt = priceSetOptions.value.find(o => o.priceSetId === priceSetId)
if (opt && !priceSetForm.value.prBegindate) {
priceSetForm.value.prBegindate = opt.effectiveDate || null
}
}
function submitPriceSetForm() {
proxy.$refs['priceSetRef'].validate(valid => {
if (!valid) return
if (priceSetForm.value.prEnddate && priceSetForm.value.prBegindate > priceSetForm.value.prEnddate) {
proxy.$modal.msgWarning('失效日期不能早于生效日期')
return
}
const payload = { ...priceSetForm.value, customerId: form.value.customerId }
priceSetSaving.value = true
saveCustomerPriceSet(payload).then(() => {
proxy.$modal.msgSuccess('保存成功')
priceSetDialogVisible.value = false
loadBoundPriceSets()
}).finally(() => {
priceSetSaving.value = false
})
})
}
function handleUnbindPriceSet(row) {
proxy.$modal.confirm(`确认解绑套餐「${row.priceSetName || row.priceSetCode || ''}」?解绑后该客户询价将不再命中此套餐。`)
.then(() => delCustomerPriceSet(row.customerPricesetId))
.then(() => {
proxy.$modal.msgSuccess('解绑成功')
loadBoundPriceSets()
}).catch(() => {})
}
/* ---------------- 用户冗余姓名 ---------------- */
function handleUserChange(uid, user, nameField) {
form.value[nameField] = user ? (user.nickName || user.userName || null) : null
@@ -968,6 +1186,7 @@ function loadDetail(id) {
form.value.attachmentList = form.value.attachmentList || []
form.value.companyList = form.value.companyList || []
form.value.contactList = form.value.contactList || []
loadBoundPriceSets()
})
}
@@ -1023,6 +1242,7 @@ function initByRoute() {
loadDetail(customerId.value).finally(() => { pageLoading.value = false })
} else {
form.value = buildEmptyForm()
boundPriceSets.value = []
}
}
@@ -1077,4 +1297,14 @@ onMounted(() => {
.bound-table {
margin-top: 4px;
}
.ps-readonly {
display: flex;
align-items: center;
gap: 10px;
color: #606266;
font-size: 13px;
&__range {
color: #909399;
}
}
</style>
@@ -11,6 +11,17 @@
<div class="head-code">客户编码:{{ customer.customerCode || route.query.customerCode || '-' }}</div>
<div class="head-right">
<el-tag type="success" size="small">本位币账户 CNY</el-tag>
<el-button
icon="Document"
@click="goBillView"
v-hasPermi="['customer:bill:list']"
>查看账单</el-button>
<el-button
type="primary"
icon="Money"
@click="openAdjustDialog()"
v-hasPermi="['customer:credit:adjust']"
>额度调整</el-button>
</div>
</div>
</el-card>
@@ -31,14 +42,23 @@
</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-label">
总额度
<el-button link type="primary" size="small" @click="openAdjustDialog('CREDIT')"
v-hasPermi="['customer:credit:adjust']">调整</el-button>
</div>
<div class="balance-value">{{ formatMoney(currentAccount.totalCredit) }}</div>
</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-label">
额外额度
<el-button link type="primary" size="small" @click="openAdjustDialog('TEMP')"
v-hasPermi="['customer:credit:adjust']">调整</el-button>
</div>
<div class="balance-value">{{ formatMoney(currentAccount.extraCredit) }}</div>
<div class="balance-sub">{{ tempCreditPeriodText }}</div>
</el-card>
</el-col>
<el-col :xs="12" :sm="8" :md="4">
@@ -225,6 +245,204 @@
@pagination="getFlowList"
/>
</el-tab-pane>
<!-- ====== 额度流水 ====== -->
<el-tab-pane label="额度流水" name="creditFlow">
<el-form :model="creditFlowQuery" :inline="true" class="search-form">
<el-form-item label="额度类型" prop="creditType">
<el-select v-model="creditFlowQuery.creditType" placeholder="全部" clearable style="width: 130px;">
<el-option v-for="opt in creditTypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
</el-form-item>
<el-form-item label="变动类型" prop="changeType">
<el-select v-model="creditFlowQuery.changeType" placeholder="全部" clearable style="width: 130px;">
<el-option v-for="opt in creditChangeTypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
</el-form-item>
<el-form-item label="日期">
<el-date-picker
v-model="creditFlowDateRange"
value-format="YYYY-MM-DD"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="getCreditFlowList">搜索</el-button>
<el-button icon="Refresh" @click="resetCreditFlowQuery">重置</el-button>
</el-form-item>
</el-form>
<el-table :data="creditFlowList" border v-loading="creditFlowLoading">
<el-table-column label="流水号" align="center" prop="flowNo" width="190" />
<el-table-column label="时间" align="center" prop="createTime" width="160">
<template #default="scope">{{ formatDateTime(scope.row.createTime) }}</template>
</el-table-column>
<el-table-column label="额度类型" align="center" prop="creditType" width="100">
<template #default="scope">
<el-tag :type="scope.row.creditType === 'TEMP' ? 'warning' : ''">
{{ formatOption(creditTypeOptions, scope.row.creditType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="变动类型" align="center" prop="changeType" width="100">
<template #default="scope">
<el-tag :type="creditChangeTag(scope.row.changeType)">
{{ formatOption(creditChangeTypeOptions, scope.row.changeType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="调整方式" align="center" prop="adjustMode" width="90">
<template #default="scope">{{ formatOption(adjustModeOptions, scope.row.adjustMode) }}</template>
</el-table-column>
<el-table-column label="方向" align="center" width="80">
<template #default="scope">
<el-tag v-if="scope.row.direction === 1" type="success">增加</el-tag>
<el-tag v-else-if="scope.row.direction === -1" type="danger">减少</el-tag>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="变动前额度" align="center" prop="beforeAmount" width="110">
<template #default="scope">{{ formatMoney(scope.row.beforeAmount) }}</template>
</el-table-column>
<el-table-column label="变动额度" align="center" width="110">
<template #default="scope">
<span :class="scope.row.direction === 1 ? 'money-in' : 'money-out'">
{{ scope.row.direction === 1 ? '+' : '-' }}{{ formatMoney(scope.row.changeAmount) }}
</span>
</template>
</el-table-column>
<el-table-column label="变动后额度" align="center" prop="afterAmount" width="110">
<template #default="scope">{{ formatMoney(scope.row.afterAmount) }}</template>
</el-table-column>
<el-table-column label="已用额度" align="center" prop="usedCredit" width="100">
<template #default="scope">{{ formatMoney(scope.row.usedCredit) }}</template>
</el-table-column>
<el-table-column label="生效日期" align="center" prop="effectiveDate" width="110">
<template #default="scope">{{ scope.row.effectiveDate || '-' }}</template>
</el-table-column>
<el-table-column label="失效日期" align="center" prop="expireDate" width="110">
<template #default="scope">{{ scope.row.expireDate || '长期' }}</template>
</el-table-column>
<el-table-column label="调整原因" align="center" prop="reason" show-overflow-tooltip min-width="160" />
<el-table-column label="操作人" align="center" prop="createBy" width="100" />
<el-table-column label="备注" align="center" prop="remark" show-overflow-tooltip min-width="160" />
</el-table>
<pagination
v-show="creditFlowTotal > 0"
:total="creditFlowTotal"
v-model:page="creditFlowQuery.pageNum"
v-model:limit="creditFlowQuery.pageSize"
@pagination="getCreditFlowList"
/>
</el-tab-pane>
<!-- ====== 账单核销(用账户余额核销应收账单) ====== -->
<el-tab-pane label="账单核销" name="billWriteoff">
<el-alert type="info" :closable="false" class="writeoff-summary">
<div class="wo-summary">
<span>待核销账单:<b>{{ unsettledList.length }}</b> 张</span>
<span>未核销总额:<b class="money-out">CNY {{ unsettledTotal }}</b></span>
<span>账户余额:<b class="money-in">CNY {{ formatMoney(currentAccount.balance) }}</b></span>
<el-button type="primary" link size="small" icon="Refresh" @click="loadBillWriteoffData">刷新</el-button>
<el-button type="primary" plain size="small" @click="goBillView"
v-hasPermi="['customer:bill:list']">查看全部账单</el-button>
</div>
</el-alert>
<div class="wo-section-title">待核销账单(已确认 / 部分核销)</div>
<el-table :data="unsettledList" border size="small" v-loading="unsettledLoading">
<el-table-column label="账单号" align="center" prop="billNo" width="170" show-overflow-tooltip />
<el-table-column label="账单名称" align="center" prop="billName" min-width="150" show-overflow-tooltip />
<el-table-column label="账期开始" align="center" prop="billPeriodStart" width="110">
<template #default="scope">{{ formatDate(scope.row.billPeriodStart) }}</template>
</el-table-column>
<el-table-column label="账期结束" align="center" prop="billPeriodEnd" width="110">
<template #default="scope">{{ formatDate(scope.row.billPeriodEnd) }}</template>
</el-table-column>
<el-table-column label="账单日期" align="center" prop="billDate" width="110">
<template #default="scope">{{ formatDate(scope.row.billDate) }}</template>
</el-table-column>
<el-table-column label="应收金额" align="center" width="120">
<template #default="scope">{{ scope.row.currency }} {{ formatMoney(scope.row.totalAmount) }}</template>
</el-table-column>
<el-table-column label="已核销" align="center" width="110">
<template #default="scope"><span class="money-in">{{ formatMoney(scope.row.writtenOffAmount) }}</span></template>
</el-table-column>
<el-table-column label="未核销" align="center" width="110">
<template #default="scope"><span class="money-out">{{ formatMoney(scope.row.unwrittenAmount) }}</span></template>
</el-table-column>
<el-table-column label="状态" align="center" width="100">
<template #default="scope">
<el-tag :type="billStatusTag(scope.row.status)" size="small">{{ billStatusLabel(scope.row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="90" fixed="right">
<template #default="scope">
<el-button link type="primary" size="small" @click="openWriteoffDialog(scope.row)"
v-hasPermi="['customer:bill:writeoff']">核销</el-button>
</template>
</el-table-column>
<template #empty>
<el-empty description="暂无待核销账单" :image-size="70" />
</template>
</el-table>
<div class="wo-section-title">余额核销记录</div>
<el-form :inline="true" class="search-form">
<el-form-item label="状态">
<el-select v-model="woQuery.status" placeholder="全部" clearable style="width: 130px;">
<el-option label="有效" value="EFFECTIVE" />
<el-option label="已反核销" value="CANCELLED" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="getWriteoffList">搜索</el-button>
<el-button icon="Refresh" @click="resetWriteoffQuery">重置</el-button>
</el-form-item>
</el-form>
<el-table :data="writeoffList" border size="small" v-loading="writeoffLoading">
<el-table-column label="核销单号" align="center" prop="writeoffNo" width="180" />
<el-table-column label="账单号" align="center" prop="billNo" width="170" show-overflow-tooltip />
<el-table-column label="账单名称" align="center" prop="billName" min-width="140" show-overflow-tooltip />
<el-table-column label="核销金额" align="center" width="120">
<template #default="scope"><span class="money-out">-{{ formatMoney(scope.row.writeoffAmount) }} {{ scope.row.currency }}</span></template>
</el-table-column>
<el-table-column label="核销日期" align="center" prop="writeoffDate" width="110">
<template #default="scope">{{ formatDate(scope.row.writeoffDate) }}</template>
</el-table-column>
<el-table-column label="状态" align="center" width="90">
<template #default="scope">
<el-tag :type="scope.row.status === 'EFFECTIVE' ? 'success' : 'info'" size="small">
{{ scope.row.status === 'EFFECTIVE' ? '有效' : '已反核销' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="备注" align="center" prop="remark" min-width="130" show-overflow-tooltip />
<el-table-column label="登记人" align="center" prop="createBy" width="90" />
<el-table-column label="登记时间" align="center" prop="createTime" width="140">
<template #default="scope">{{ formatDateTime(scope.row.createTime) }}</template>
</el-table-column>
<el-table-column label="反核销原因" align="center" prop="cancelRemark" min-width="130" show-overflow-tooltip />
<el-table-column label="操作" align="center" width="90" fixed="right">
<template #default="scope">
<el-button v-if="scope.row.status === 'EFFECTIVE'" link type="warning" size="small"
@click="openCancelWriteoffDialog(scope.row)"
v-hasPermi="['customer:bill:writeoff:cancel']">反核销</el-button>
<span v-else>-</span>
</template>
</el-table-column>
</el-table>
<pagination
v-show="writeoffTotal > 0"
:total="writeoffTotal"
v-model:page="woQuery.pageNum"
v-model:limit="woQuery.pageSize"
@pagination="getWriteoffList"
/>
</el-tab-pane>
</el-tabs>
</el-card>
@@ -402,6 +620,183 @@
<el-button @click="viewDialog.visible = false">关 闭</el-button>
</template>
</el-dialog>
<!-- ====== 额度调整弹窗 ====== -->
<el-dialog
:title="creditDialog.form.creditType === 'TEMP' ? '临时额度调整' : '信用额度调整'"
v-model="creditDialog.visible"
width="600px"
append-to-body
@closed="resetCreditForm"
>
<el-alert type="info" :closable="false" class="credit-tip">
<div>当前{{ creditDialog.form.creditType === 'TEMP' ? '临时额度' : '信用额度' }}:
<b>{{ formatMoney(creditBefore) }}</b>,已用额度:<b>{{ formatMoney(currentAccount.usedCredit) }}</b></div>
</el-alert>
<el-form ref="creditFormRef" :model="creditDialog.form" :rules="creditRules" label-width="100px">
<el-row>
<el-col :span="12">
<el-form-item label="额度类型" prop="creditType">
<el-radio-group v-model="creditDialog.form.creditType" @change="onCreditTypeChange">
<el-radio value="CREDIT">信用额度(长期)</el-radio>
<el-radio value="TEMP">临时额度</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="调整方式" prop="adjustMode">
<el-radio-group v-model="creditDialog.form.adjustMode">
<el-radio value="SET">设置为</el-radio>
<el-radio value="ADD">增加</el-radio>
<el-radio value="SUB">减少</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="调整金额" prop="changeAmount">
<el-input-number
v-model="creditDialog.form.changeAmount"
:precision="2"
:step="1000"
:min="0.01"
controls-position="right"
style="width: 100%;"
/>
</el-form-item>
</el-col>
<template v-if="creditDialog.form.creditType === 'TEMP'">
<el-col :span="12">
<el-form-item label="生效日期" prop="effectiveDate">
<el-date-picker
v-model="creditDialog.form.effectiveDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="默认今天"
:disabled-date="afterToday"
style="width: 100%;"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="失效日期" prop="expireDate">
<el-date-picker
v-model="creditDialog.form.expireDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="不填为长期有效"
style="width: 100%;"
/>
<div class="rate-tip">失效日当天仍有效,次日访问账户时自动清零并记到期流水</div>
</el-form-item>
</el-col>
</template>
<el-col :span="24">
<el-form-item label="调整原因" prop="reason">
<el-input
v-model="creditDialog.form.reason"
type="textarea"
:rows="2"
maxlength="255"
show-word-limit
placeholder="请必填,例如:客户旺季临时授信 / 年审调整长期额度"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div class="credit-preview">
<div>调整后{{ creditDialog.form.creditType === 'TEMP' ? '临时额度' : '信用额度' }}:
<b :class="creditAfter < 0 ? 'money-out' : 'money-in'">{{ formatMoney(creditAfter) }}</b>
</div>
<div>调整后授信总额:<b>{{ formatMoney(totalCreditAfter) }}</b>
(可用 = 授信总额 - 已用 {{ formatMoney(currentAccount.usedCredit) }})</div>
<el-alert
v-if="creditAfter < 0"
type="error" :closable="false"
title="调整后额度不能为负数" class="credit-alert"
/>
<el-alert
v-else-if="totalCreditAfter < (Number(currentAccount.usedCredit) || 0)"
type="error" :closable="false"
title="授信总额不能小于已用额度" class="credit-alert"
/>
</div>
<template #footer>
<el-button type="primary" :loading="creditSubmitLoading" @click="submitAdjustCredit">确 定</el-button>
<el-button @click="creditDialog.visible = false">取 消</el-button>
</template>
</el-dialog>
<!-- ====== 余额核销账单弹窗 ====== -->
<el-dialog title="余额核销账单" v-model="woDialog.visible" width="560px" append-to-body>
<el-descriptions :column="1" border size="small" v-if="woDialog.bill">
<el-descriptions-item label="账单号">{{ woDialog.bill.billNo }}</el-descriptions-item>
<el-descriptions-item label="账单名称">{{ woDialog.bill.billName || '-' }}</el-descriptions-item>
<el-descriptions-item label="账单未核销金额">
<span class="money-out">CNY {{ formatMoney(woDialog.bill.unwrittenAmount) }}</span>
</el-descriptions-item>
<el-descriptions-item label="账户可用余额">
<span class="money-in">CNY {{ formatMoney(currentAccount.balance) }}</span>
</el-descriptions-item>
</el-descriptions>
<el-form :model="woDialog.form" label-width="110px" class="wo-form">
<el-form-item label="本次核销金额" required>
<el-input-number
v-model="woDialog.form.writeoffAmount"
:precision="2"
:step="100"
:min="0.01"
controls-position="right"
style="width: 220px;"
/>
<span class="wo-form-unit">CNY</span>
</el-form-item>
<el-form-item label="核销日期">
<el-date-picker
v-model="woDialog.form.writeoffDate"
type="date"
value-format="YYYY-MM-DD"
placeholder="默认今天"
style="width: 220px;"
/>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="woDialog.form.remark" type="textarea" :rows="2" maxlength="255" show-word-limit
placeholder="选填,最多255字" />
</el-form-item>
</el-form>
<el-alert type="warning" :closable="false">
<template title>
核销后账户余额变为 CNY {{ woAfterBalance }},同步释放等额已用额度;余额不足时无法提交。
</template>
</el-alert>
<template #footer>
<el-button type="primary" :loading="woSubmitLoading" @click="submitWriteoff">确认核销</el-button>
<el-button @click="woDialog.visible = false">取 消</el-button>
</template>
</el-dialog>
<!-- ====== 反核销弹窗 ====== -->
<el-dialog title="反核销(取消余额核销)" v-model="cancelWoDialog.visible" width="480px" append-to-body>
<el-alert type="warning" :closable="false" class="credit-tip">
<span v-if="cancelWoDialog.row">
反核销后,核销金额 CNY {{ formatMoney(cancelWoDialog.row.writeoffAmount) }} 将退回客户账户余额,
账单「{{ cancelWoDialog.row.billNo }}」恢复为未核销,已用额度同步加回。
</span>
</el-alert>
<el-form label-width="100px" class="wo-form">
<el-form-item label="反核销原因" required>
<el-input v-model="cancelWoDialog.cancelRemark" type="textarea" :rows="3" maxlength="255" show-word-limit
placeholder="请填写反核销原因(必填)" />
</el-form-item>
</el-form>
<template #footer>
<el-button type="warning" :loading="cancelWoLoading" @click="submitCancelWriteoff">确认反核销</el-button>
<el-button @click="cancelWoDialog.visible = false">取 消</el-button>
</template>
</el-dialog>
</div>
</template>
@@ -409,11 +804,13 @@
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 { listAccount, listReceipt, getReceipt, addReceipt, confirmReceipt, cancelReceipt, listFlow, listCreditFlow, adjustCredit } from "@/api/customersuppliermanagement/customermoney/customermoney.js"
import { listUnsettledBill, listBalanceWriteoff, balanceWriteoff, cancelBalanceWriteoff } from "@/api/customersuppliermanagement/customermoney/customerbill.js"
import { convertToBase } from "@/api/data/exchangeRate"
import { getToken } from "@/utils/auth"
import { useRoute } from 'vue-router'
import { nextTick } from 'vue'
const route = useRoute()
const router = useRouter()
@@ -472,9 +869,28 @@ function receiptTypeTag(v) {
function receiptStatusTag(v) {
return { PENDING: 'warning', CONFIRMED: 'success', CANCELLED: 'danger' }[v] || 'info'
}
function bizTypeTag(v) {
const bizTypeTag = function(v) {
return { RECEIPT: 'success', BILL_PAY: 'danger', REFUND: 'warning', ADJUST: 'info', FREEZE: 'info', UNFREEZE: '' }[v] || 'info'
}
/* ================= 额度相关枚举 ================= */
const creditTypeOptions = [
{ value: 'CREDIT', label: '信用额度' },
{ value: 'TEMP', label: '临时额度' }
]
const creditChangeTypeOptions = [
{ value: 'INIT', label: '初始化' },
{ value: 'ADJUST', label: '手工调整' },
{ value: 'EXPIRE', label: '到期失效' }
]
const adjustModeOptions = [
{ value: 'SET', label: '设置为' },
{ value: 'ADD', label: '增加' },
{ value: 'SUB', label: '减少' }
]
function creditChangeTag(v) {
return { INIT: '', ADJUST: 'primary', EXPIRE: 'danger' }[v] || 'info'
}
function formatMoney(val) {
const num = Number(val)
return isNaN(num) ? '0.00' : num.toFixed(2)
@@ -506,6 +922,152 @@ const availableCredit = computed(() => {
return formatMoney(total + extra - used)
})
/** 临时额度有效期文案 */
const tempCreditPeriodText = computed(() => {
const a = currentAccount.value
const extra = Number(a.extraCredit) || 0
if (extra <= 0) return ''
if (!a.extraExpireDate) return '长期有效'
const today = proxy.parseTime(new Date(), '{y}-{m}-{d}')
if (a.extraExpireDate < today) return '已过期'
return '有效期至 ' + a.extraExpireDate
})
/* ============ 额度调整弹窗 ============ */
const creditFormRef = ref(null)
const creditSubmitLoading = ref(false)
const creditDialog = reactive({
visible: false,
form: {}
})
const creditRules = {
creditType: [{ required: true, message: '请选择额度类型', trigger: 'change' }],
adjustMode: [{ required: true, message: '请选择调整方式', trigger: 'change' }],
changeAmount: [{ required: true, message: '调整金额必须大于0', trigger: 'blur' }],
reason: [{ required: true, message: '请填写调整原因', trigger: 'blur' }]
}
/** 禁止选择晚于今天的日期(不允许预约未来生效) */
function afterToday(date) {
const today = new Date()
today.setHours(23, 59, 59, 999)
return date.getTime() > today.getTime()
}
function defaultCreditForm(creditType) {
return {
customerId: route.query.customerId || null,
creditType: creditType || 'CREDIT',
adjustMode: 'ADD',
changeAmount: null,
reason: '',
effectiveDate: proxy.parseTime(new Date(), '{y}-{m}-{d}'),
expireDate: ''
}
}
function openAdjustDialog(creditType) {
const customerId = Number(route.query.customerId)
if (!customerId) {
proxy.$modal.msgError('缺少客户ID,请从客户列表进入客户余额页')
return
}
resetCreditForm()
creditDialog.form = defaultCreditForm(creditType)
creditDialog.visible = true
}
function resetCreditForm() {
creditDialog.form = defaultCreditForm()
proxy.resetForm('creditFormRef')
}
/** 切换额度类型:清掉校验红框,临时额度默认生效日为今天 */
function onCreditTypeChange() {
creditDialog.form.changeAmount = null
if (creditDialog.form.creditType === 'TEMP' && !creditDialog.form.effectiveDate) {
creditDialog.form.effectiveDate = proxy.parseTime(new Date(), '{y}-{m}-{d}')
}
nextTick(() => proxy.clearValidate('creditFormRef'))
}
/** 当前被调整额度(调整前) */
const creditBefore = computed(() => {
const a = currentAccount.value
return Number(creditDialog.form.creditType === 'TEMP' ? a.extraCredit : a.totalCredit) || 0
})
/** 调整后被调额度 */
const creditAfter = computed(() => {
const input = Number(creditDialog.form.changeAmount) || 0
const mode = creditDialog.form.adjustMode
if (mode === 'SET') return input
return mode === 'SUB' ? creditBefore.value - input : creditBefore.value + input
})
/** 调整后授信总额(信用+临时) */
const totalCreditAfter = computed(() => {
const a = currentAccount.value
const other = Number(creditDialog.form.creditType === 'TEMP' ? a.totalCredit : a.extraCredit) || 0
return other + creditAfter.value
})
function submitAdjustCredit() {
creditFormRef.value.validate(valid => {
if (!valid) return
const f = creditDialog.form
const amount = Number(f.changeAmount)
if (!(amount > 0)) {
proxy.$modal.msgError('调整金额必须大于0')
return
}
if (f.creditType === 'TEMP') {
if (f.expireDate && f.effectiveDate && f.expireDate <= f.effectiveDate) {
proxy.$modal.msgError('失效日期必须晚于生效日期')
return
}
}
if (creditAfter.value < 0) {
proxy.$modal.msgError('调整后额度不能为负数')
return
}
const used = Number(currentAccount.value.usedCredit) || 0
if (totalCreditAfter.value < used) {
proxy.$modal.msgError('调整后授信总额不能小于已用额度 ' + formatMoney(used))
return
}
if (creditAfter.value === creditBefore.value) {
proxy.$modal.msgError('额度未发生变化,无需调整')
return
}
const payload = {
customerId: Number(f.customerId) || Number(route.query.customerId) || null,
creditType: f.creditType,
adjustMode: f.adjustMode,
changeAmount: amount,
reason: f.reason
}
if (!payload.customerId) {
proxy.$modal.msgError('缺少客户ID,请从客户列表进入客户余额页')
return
}
if (f.creditType === 'TEMP') {
payload.effectiveDate = f.effectiveDate || null
payload.expireDate = f.expireDate || null
}
creditSubmitLoading.value = true
adjustCredit(payload).then(() => {
proxy.$modal.msgSuccess('额度调整成功')
creditDialog.visible = false
loadCustomer()
loadAccounts()
getCreditFlowList()
}).finally(() => {
creditSubmitLoading.value = false
})
})
}
function loadCustomer() {
const customerId = route.query.customerId
if (!customerId) return
@@ -752,6 +1314,208 @@ function resetFlowQuery() {
getFlowList()
}
/* ================= 额度流水 ================= */
const creditFlowList = ref([])
const creditFlowTotal = ref(0)
const creditFlowLoading = ref(false)
const creditFlowDateRange = ref([])
const creditFlowQuery = reactive({
pageNum: 1,
pageSize: 10,
customerId: route.query.customerId,
creditType: '',
changeType: ''
})
function getCreditFlowList() {
creditFlowLoading.value = true
const params = proxy.addDateRange({ ...creditFlowQuery }, creditFlowDateRange.value)
listCreditFlow(params).then(res => {
creditFlowList.value = res.rows || []
creditFlowTotal.value = res.total || 0
}).finally(() => {
creditFlowLoading.value = false
})
}
function resetCreditFlowQuery() {
creditFlowQuery.creditType = ''
creditFlowQuery.changeType = ''
creditFlowDateRange.value = []
creditFlowQuery.pageNum = 1
getCreditFlowList()
}
/* ================= 账单核销 ================= */
const billStatusMap = {
DRAFT: { label: '草稿', type: 'info' },
CONFIRMED: { label: '已确认', type: 'warning' },
PARTIAL_WRITTEN_OFF: { label: '部分核销', type: 'primary' },
WRITTEN_OFF: { label: '已核销', type: 'success' },
VOID: { label: '已作废', type: 'danger' }
}
function billStatusLabel(v) {
return billStatusMap[v]?.label || v || '-'
}
function billStatusTag(v) {
return billStatusMap[v]?.type || 'info'
}
function formatDate(val) {
if (!val) return '-'
return String(val).substring(0, 10)
}
function goBillView() {
router.push({
path: '/customersuppliermanagement/customerbill',
query: { customerId: route.query.customerId || '' }
})
}
const unsettledList = ref([])
const unsettledLoading = ref(false)
/** 未核销账单合计 */
const unsettledTotal = computed(() => {
const sum = unsettledList.value.reduce((s, r) => s + (Number(r.unwrittenAmount) || 0), 0)
return formatMoney(sum)
})
const writeoffList = ref([])
const writeoffTotal = ref(0)
const writeoffLoading = ref(false)
const woQuery = reactive({
pageNum: 1,
pageSize: 10,
customerId: route.query.customerId,
status: ''
})
let billWriteoffLoaded = false
function loadUnsettledBills() {
const customerId = route.query.customerId
if (!customerId) return
unsettledLoading.value = true
listUnsettledBill(customerId).then(res => {
unsettledList.value = res.data || []
}).finally(() => {
unsettledLoading.value = false
})
}
function getWriteoffList() {
const customerId = route.query.customerId
if (!customerId) return
writeoffLoading.value = true
woQuery.customerId = customerId
listBalanceWriteoff({ ...woQuery }).then(res => {
writeoffList.value = res.rows || []
writeoffTotal.value = res.total || 0
}).finally(() => {
writeoffLoading.value = false
})
}
function resetWriteoffQuery() {
woQuery.status = ''
woQuery.pageNum = 1
getWriteoffList()
}
function loadBillWriteoffData() {
loadAccounts()
loadUnsettledBills()
getWriteoffList()
}
/* ---- 核销弹窗 ---- */
const woSubmitLoading = ref(false)
const woDialog = reactive({ visible: false, bill: null, form: {} })
/** 核销后余额预览(不为负则可提交) */
const woAfterBalance = computed(() => {
const balance = Number(currentAccount.value.balance) || 0
const amount = Number(woDialog.form.writeoffAmount) || 0
return formatMoney(balance - amount)
})
function openWriteoffDialog(row) {
// 先刷新账户余额,避免用过期余额核销
listAccount(route.query.customerId).then(res => {
accountList.value = res.data || []
})
woDialog.bill = row
const balance = Number(currentAccount.value.balance) || 0
const unwritten = Number(row.unwrittenAmount) || 0
woDialog.form = {
arBillId: row.arBillId,
customerId: Number(route.query.customerId),
writeoffAmount: Math.min(unwritten, balance > 0 ? balance : unwritten),
writeoffDate: proxy.parseTime(new Date(), '{y}-{m}-{d}'),
remark: ''
}
woDialog.visible = true
}
function submitWriteoff() {
const bill = woDialog.bill
const amount = Number(woDialog.form.writeoffAmount)
if (!(amount > 0)) {
proxy.$modal.msgError('核销金额必须大于0')
return
}
const unwritten = Number(bill.unwrittenAmount) || 0
if (amount > unwritten) {
proxy.$modal.msgError('核销金额不能超过账单未核销金额 ' + formatMoney(unwritten))
return
}
const balance = Number(currentAccount.value.balance) || 0
if (amount > balance) {
proxy.$modal.msgError('账户余额不足,当前余额 CNY ' + formatMoney(balance) + ',请先登记收款并审核')
return
}
woSubmitLoading.value = true
balanceWriteoff(woDialog.form).then(() => {
proxy.$modal.msgSuccess('核销成功:账单已核销 CNY ' + formatMoney(amount))
woDialog.visible = false
loadBillWriteoffData()
getFlowList()
}).finally(() => {
woSubmitLoading.value = false
})
}
/* ---- 反核销弹窗 ---- */
const cancelWoLoading = ref(false)
const cancelWoDialog = reactive({ visible: false, row: null, cancelRemark: '' })
function openCancelWriteoffDialog(row) {
cancelWoDialog.row = row
cancelWoDialog.cancelRemark = ''
cancelWoDialog.visible = true
}
function submitCancelWriteoff() {
const remark = cancelWoDialog.cancelRemark?.trim()
if (!remark) {
proxy.$modal.msgError('请填写反核销原因')
return
}
cancelWoLoading.value = true
cancelBalanceWriteoff(cancelWoDialog.row.writeoffId, remark).then(() => {
proxy.$modal.msgSuccess('反核销成功,余额已退回')
cancelWoDialog.visible = false
loadBillWriteoffData()
getFlowList()
}).finally(() => {
cancelWoLoading.value = false
})
}
watch(activeTab, (tab) => {
if (tab === 'billWriteoff' && !billWriteoffLoaded) {
billWriteoffLoaded = true
loadBillWriteoffData()
}
})
/* ================= 附件上传 ================= */
const uploadFileUrl = ref(baseUrl + "/common/upload")
const uploadHeaders = ref({ Authorization: "Bearer " + getToken() })
@@ -848,6 +1612,8 @@ function initPage() {
viewDialog.attachments = []
receiptDateRange.value = []
flowDateRange.value = []
creditFlowDateRange.value = []
creditDialog.visible = false
receiptQuery.customerId = customerId
receiptQuery.receiptType = ''
receiptQuery.status = ''
@@ -856,15 +1622,35 @@ function initPage() {
flowQuery.bizType = ''
flowQuery.direction = ''
flowQuery.pageNum = 1
creditFlowQuery.customerId = customerId
creditFlowQuery.creditType = ''
creditFlowQuery.changeType = ''
creditFlowQuery.pageNum = 1
receiptList.value = []
receiptTotal.value = 0
flowList.value = []
flowTotal.value = 0
creditFlowList.value = []
creditFlowTotal.value = 0
unsettledList.value = []
writeoffList.value = []
writeoffTotal.value = 0
woQuery.customerId = customerId
woQuery.status = ''
woQuery.pageNum = 1
woDialog.visible = false
cancelWoDialog.visible = false
billWriteoffLoaded = false
if (!customerId) return
loadCustomer()
loadAccounts()
getReceiptList()
getFlowList()
getCreditFlowList()
if (activeTab.value === 'billWriteoff') {
billWriteoffLoaded = true
loadBillWriteoffData()
}
}
onMounted(initPage)
@@ -916,6 +1702,31 @@ watch(() => route.query.customerId, (newId, oldId) => {
color: var(--el-text-color-secondary);
font-size: 13px;
margin-bottom: 8px;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
}
.balance-sub {
margin-top: 6px;
font-size: 12px;
color: var(--el-text-color-secondary);
min-height: 16px;
}
.credit-tip {
margin-bottom: 12px;
}
.credit-preview {
margin-top: 4px;
padding: 10px 14px;
background: var(--el-fill-color-light);
border-radius: 4px;
font-size: 13px;
line-height: 1.8;
}
.credit-alert {
margin-top: 8px;
margin-bottom: 0;
}
.balance-value {
font-size: 22px;
@@ -952,4 +1763,28 @@ watch(() => route.query.customerId, (newId, oldId) => {
font-weight: 600;
margin: 12px 0 8px;
}
.writeoff-summary {
margin-bottom: 12px;
}
.wo-summary {
display: flex;
align-items: center;
gap: 18px;
flex-wrap: wrap;
font-size: 13px;
}
.wo-section-title {
font-weight: 600;
font-size: 14px;
margin: 14px 0 8px;
padding-left: 8px;
border-left: 3px solid var(--el-color-primary);
}
.wo-form {
margin-top: 14px;
}
.wo-form-unit {
margin-left: 8px;
color: var(--el-text-color-secondary);
}
</style>