客户往来多维度明细功能前端联调文档
优化客户往来功能,新增多维度明细查询接口,支持产品明细和发货明细维度
涉及页面
- 营销管理 / 客户往来 - 客户往来列表页
- 营销管理 / 客户往来 - 客户往来详情页(新增)
- 营销管理 / 客户往来 - 产品明细Tab(新增)
- 营销管理 / 客户往来 - 发货明细Tab(新增)
API
1. 客户往来列表(原有接口,未变更)
| 方法 |
路径 |
说明 |
| GET |
/metricStatistics/customewTransactions |
客户往来列表 |
请求参数:
| 参数 |
类型 |
必填 |
说明 |
| pageNum |
Long |
否 |
页码,默认1 |
| pageSize |
Long |
否 |
每页条数,默认10 |
| customerName |
String |
否 |
客户名称(模糊搜索) |
响应字段:
| 字段 |
类型 |
说明 |
| customerId |
Long |
客户ID |
| customerName |
String |
客户名称 |
| contractAmounts |
BigDecimal |
合同总金额 |
| receiptPaymentAmount |
BigDecimal |
收款金额 |
| receiptableAmount |
BigDecimal |
应收金额 |
1.5 客户往来明细(原有接口,新增字段)
| 方法 |
路径 |
说明 |
| GET |
/metricStatistics/customewTransactionsDetails |
客户往来明细(合同维度) |
请求参数:
| 参数 |
类型 |
必填 |
说明 |
| customerId |
Long |
是 |
客户ID |
| pageNum |
Long |
否 |
页码,默认1 |
| pageSize |
Long |
否 |
每页条数,默认10 |
响应字段:
| 字段 |
类型 |
说明 |
| salesLedgerId |
Long |
销售台账ID |
| salesContractNo |
String |
销售合同号 |
| executionDate |
LocalDate |
合同签订日期 |
| contractAmount |
BigDecimal |
合同金额 |
| productNames |
String |
产品名称列表(逗号分隔)【新增】 |
| receiptPaymentAmount |
BigDecimal |
收款金额 |
| receiptableAmount |
BigDecimal |
应收金额 |
响应示例:
{
"code": 200,
"msg": "操作成功",
"data": {
"records": [
{
"salesLedgerId": 1,
"salesContractNo": "HT-2026-001",
"executionDate": "2026-06-01",
"contractAmount": 50000.00,
"productNames": "产品A,产品B,产品C",
"receiptPaymentAmount": 30000.00,
"receiptableAmount": 20000.00
}
],
"total": 10
}
}
2. 客户往来统计汇总(新增)
| 方法 |
路径 |
说明 |
| GET |
/metricStatistics/customerTransactionsSummary |
客户往来统计汇总 |
请求参数:
| 参数 |
类型 |
必填 |
说明 |
| customerId |
Long |
是 |
客户ID |
响应字段:
| 字段 |
类型 |
说明 |
| customerId |
Long |
客户ID |
| customerName |
String |
客户名称 |
| contractAmounts |
BigDecimal |
合同总金额 |
| contractCount |
Integer |
合同数量 |
| productCount |
Integer |
产品种类数 |
| shippedAmounts |
BigDecimal |
发货总金额 |
| shippedQuantity |
BigDecimal |
发货总数量 |
| receivedAmounts |
BigDecimal |
收款金额 |
| receivableAmounts |
BigDecimal |
应收金额 |
| returnAmounts |
BigDecimal |
退货金额 |
| unshippedAmounts |
BigDecimal |
未发货金额 |
| receivedRate |
BigDecimal |
收款率(%) |
| shippedRate |
BigDecimal |
发货率(%) |
响应示例:
{
"code": 200,
"msg": "操作成功",
"data": {
"customerId": 1,
"customerName": "客户A",
"contractAmounts": 100000.00,
"contractCount": 5,
"productCount": 12,
"shippedAmounts": 80000.00,
"shippedQuantity": 500,
"receivedAmounts": 60000.00,
"receivableAmounts": 20000.00,
"returnAmounts": 5000.00,
"unshippedAmounts": 20000.00,
"receivedRate": 75.00,
"shippedRate": 80.00
}
}
3. 客户往来产品明细(新增)
| 方法 |
路径 |
说明 |
| GET |
/metricStatistics/customerTransactionsProducts |
客户往来产品明细 |
请求参数:
| 参数 |
类型 |
必填 |
说明 |
| customerId |
Long |
是 |
客户ID |
| salesLedgerId |
Long |
否 |
销售台账ID(可选,用于筛选某合同) |
| pageNum |
Long |
否 |
页码,默认1 |
| pageSize |
Long |
否 |
每页条数,默认10 |
响应字段:
| 字段 |
类型 |
说明 |
| salesLedgerId |
Long |
销售台账ID |
| salesContractNo |
String |
销售合同号 |
| productId |
Long |
产品ID |
| productName |
String |
产品名称 |
| model |
String |
规格型号 |
| unit |
String |
单位 |
| contractQuantity |
BigDecimal |
合同数量 |
| taxInclusiveUnitPrice |
BigDecimal |
合同单价(含税) |
| contractAmount |
BigDecimal |
合同金额 |
| shippedQuantity |
BigDecimal |
已发货数量 |
| shippedAmount |
BigDecimal |
已发货金额 |
| receivedAmount |
BigDecimal |
已收款金额 |
| receivableAmount |
BigDecimal |
应收金额 |
响应示例:
{
"code": 200,
"msg": "操作成功",
"data": {
"records": [
{
"salesLedgerId": 1,
"salesContractNo": "HT-2026-001",
"productId": 10,
"productName": "产品A",
"model": "规格1",
"unit": "件",
"contractQuantity": 100,
"taxInclusiveUnitPrice": 50.00,
"contractAmount": 5000.00,
"shippedQuantity": 80,
"shippedAmount": 4000.00,
"receivedAmount": 3000.00,
"receivableAmount": 1000.00
}
],
"total": 25,
"pageNum": 1,
"pageSize": 10
}
}
4. 客户往来发货明细(新增)
| 方法 |
路径 |
说明 |
| GET |
/metricStatistics/customerTransactionsShipments |
客户往来发货明细 |
请求参数:
| 参数 |
类型 |
必填 |
说明 |
| customerId |
Long |
是 |
客户ID |
| salesLedgerId |
Long |
否 |
销售台账ID(可选,用于筛选某合同) |
| pageNum |
Long |
否 |
页码,默认1 |
| pageSize |
Long |
否 |
每页条数,默认10 |
响应字段:
| 字段 |
类型 |
说明 |
| salesLedgerId |
Long |
销售台账ID |
| salesContractNo |
String |
销售合同号 |
| shippingId |
Long |
发货单ID |
| shippingNo |
String |
发货单号 |
| productName |
String |
产品名称 |
| model |
String |
规格型号 |
| shippingQuantity |
BigDecimal |
发货数量 |
| shippingAmount |
BigDecimal |
发货金额(含税) |
| batchNo |
String |
出库批号 |
| shippingDate |
LocalDate |
发货日期 |
| approvalStatus |
Integer |
审批状态(0待审/1已审) |
| receivedAmount |
BigDecimal |
已收款金额 |
| receivableAmount |
BigDecimal |
应收金额 |
响应示例:
{
"code": 200,
"msg": "操作成功",
"data": {
"records": [
{
"salesLedgerId": 1,
"salesContractNo": "HT-2026-001",
"shippingId": 100,
"shippingNo": "FH-2026-001",
"productName": "产品A",
"model": "规格1",
"shippingQuantity": 50,
"shippingAmount": 2500.00,
"batchNo": "20260618001",
"shippingDate": "2026-06-18",
"approvalStatus": 1,
"receivedAmount": 2000.00,
"receivableAmount": 500.00
}
],
"total": 30,
"pageNum": 1,
"pageSize": 10
}
}
前端页面设计
1. 客户往来列表页(优化)
<template>
<div class="app-container">
<!-- 搜索栏 -->
<el-form :model="queryParams" ref="queryForm" :inline="true">
<el-form-item label="客户名称" prop="customerName">
<el-input v-model="queryParams.customerName" placeholder="请输入客户名称" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="getList">搜索</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<!-- 数据表格 -->
<el-table :data="list" v-loading="loading">
<el-table-column label="客户名称" prop="customerName" />
<el-table-column label="合同总金额" prop="contractAmounts" align="right">
<template #default="{ row }">
{{ formatMoney(row.contractAmounts) }}
</template>
</el-table-column>
<el-table-column label="收款金额" prop="receiptPaymentAmount" align="right">
<template #default="{ row }">
{{ formatMoney(row.receiptPaymentAmount) }}
</template>
</el-table-column>
<el-table-column label="应收金额" prop="receiptableAmount" align="right">
<template #default="{ row }">
<span :class="{ 'text-danger': row.receiptableAmount > 0 }">
{{ formatMoney(row.receiptableAmount) }}
</span>
</template>
</el-table-column>
<el-table-column label="操作" width="150" align="center">
<template #default="{ row }">
<el-button type="text" @click="viewDetail(row)">查看明细</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-show="total > 0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
</div>
</template>
<script>
export default {
name: 'CustomerTransactions',
data() {
return {
loading: false,
list: [],
total: 0,
queryParams: {
pageNum: 1,
pageSize: 10,
customerName: ''
}
}
},
created() {
this.getList()
},
methods: {
getList() {
this.loading = true
this.$axios.get('/metricStatistics/customewTransactions', { params: this.queryParams })
.then(res => {
this.list = res.data.records
this.total = res.data.total
})
.finally(() => {
this.loading = false
})
},
resetQuery() {
this.queryParams.customerName = ''
this.getList()
},
formatMoney(value) {
if (!value) return '0.00'
return Number(value).toFixed(2)
},
viewDetail(row) {
this.$router.push({ path: '/sales/customerTransactions/detail', query: { customerId: row.customerId } })
}
}
}
</script>
2. 客户往来详情页(新增)
<template>
<div class="app-container">
<!-- 顶部统计卡片 -->
<el-card class="summary-card">
<div slot="header">
<span>{{ summary.customerName }} - 往来统计</span>
</div>
<el-row :gutter="20">
<el-col :span="4">
<div class="stat-item">
<div class="stat-label">合同总金额</div>
<div class="stat-value">{{ formatMoney(summary.contractAmounts) }}</div>
</div>
</el-col>
<el-col :span="4">
<div class="stat-item">
<div class="stat-label">合同数量</div>
<div class="stat-value">{{ summary.contractCount }}份</div>
</div>
</el-col>
<el-col :span="4">
<div class="stat-item">
<div class="stat-label">产品种类</div>
<div class="stat-value">{{ summary.productCount }}种</div>
</div>
</el-col>
<el-col :span="4">
<div class="stat-item">
<div class="stat-label">发货金额</div>
<div class="stat-value">{{ formatMoney(summary.shippedAmounts) }}</div>
</div>
</el-col>
<el-col :span="4">
<div class="stat-item">
<div class="stat-label">收款金额</div>
<div class="stat-value text-success">{{ formatMoney(summary.receivedAmounts) }}</div>
</div>
</el-col>
<el-col :span="4">
<div class="stat-item">
<div class="stat-label">应收金额</div>
<div class="stat-value text-danger">{{ formatMoney(summary.receivableAmounts) }}</div>
</div>
</el-col>
</el-row>
<el-row :gutter="20" style="margin-top: 15px;">
<el-col :span="4">
<div class="stat-item">
<div class="stat-label">退货金额</div>
<div class="stat-value">{{ formatMoney(summary.returnAmounts) }}</div>
</div>
</el-col>
<el-col :span="4">
<div class="stat-item">
<div class="stat-label">未发货金额</div>
<div class="stat-value text-warning">{{ formatMoney(summary.unshippedAmounts) }}</div>
</div>
</el-col>
<el-col :span="4">
<div class="stat-item">
<div class="stat-label">收款率</div>
<div class="stat-value">
<el-progress :percentage="summary.receivedRate || 0" :stroke-width="18" />
</div>
</div>
</el-col>
<el-col :span="4">
<div class="stat-item">
<div class="stat-label">发货率</div>
<div class="stat-value">
<el-progress :percentage="summary.shippedRate || 0" :stroke-width="18" color="#67c23a" />
</div>
</div>
</el-col>
</el-row>
</el-card>
<!-- Tab 切换 -->
<el-tabs v-model="activeTab" @tab-click="handleTabChange">
<el-tab-pane label="产品明细" name="products">
<product-table :customerId="customerId" />
</el-tab-pane>
<el-tab-pane label="发货明细" name="shipments">
<shipment-table :customerId="customerId" />
</el-tab-pane>
<el-tab-pane label="合同明细" name="contracts">
<contract-table :customerId="customerId" />
</el-tab-pane>
</el-tabs>
</div>
</template>
<script>
import ProductTable from './components/ProductTable.vue'
import ShipmentTable from './components/ShipmentTable.vue'
import ContractTable from './components/ContractTable.vue'
export default {
name: 'CustomerTransactionsDetail',
components: { ProductTable, ShipmentTable, ContractTable },
data() {
return {
customerId: null,
summary: {},
activeTab: 'products'
}
},
created() {
this.customerId = this.$route.query.customerId
if (this.customerId) {
this.getSummary()
}
},
methods: {
getSummary() {
this.$axios.get('/metricStatistics/customerTransactionsSummary', {
params: { customerId: this.customerId }
}).then(res => {
this.summary = res.data
})
},
formatMoney(value) {
if (!value) return '0.00'
return Number(value).toFixed(2)
},
handleTabChange(tab) {
// Tab 切换时刷新子组件数据
}
}
}
</script>
<style scoped>
.summary-card {
margin-bottom: 20px;
}
.stat-item {
text-align: center;
}
.stat-label {
font-size: 14px;
color: #909399;
}
.stat-value {
font-size: 18px;
font-weight: bold;
margin-top: 5px;
}
.text-success {
color: #67c23a;
}
.text-danger {
color: #f56c6c;
}
.text-warning {
color: #e6a23c;
}
</style>
3. 产品明细组件 (ProductTable.vue)
<template>
<div>
<!-- 筛选 -->
<el-form :inline="true" size="small">
<el-form-item label="合同号">
<el-select v-model="filterData.salesLedgerId" clearable placeholder="全部合同" @change="getList">
<el-option v-for="item in contractList" :key="item.id" :label="item.salesContractNo" :value="item.id" />
</el-select>
</el-form-item>
</el-form>
<!-- 表格 -->
<el-table :data="list" v-loading="loading" size="small">
<el-table-column label="合同号" prop="salesContractNo" width="150" />
<el-table-column label="产品名称" prop="productName" />
<el-table-column label="规格型号" prop="model" width="120" />
<el-table-column label="单位" prop="unit" width="80" />
<el-table-column label="合同数量" prop="contractQuantity" align="right" width="100" />
<el-table-column label="合同单价" prop="taxInclusiveUnitPrice" align="right" width="100">
<template #default="{ row }">
{{ formatMoney(row.taxInclusiveUnitPrice) }}
</template>
</el-table-column>
<el-table-column label="合同金额" prop="contractAmount" align="right" width="120">
<template #default="{ row }">
{{ formatMoney(row.contractAmount) }}
</template>
</el-table-column>
<el-table-column label="已发货数量" prop="shippedQuantity" align="right" width="100" />
<el-table-column label="已发货金额" prop="shippedAmount" align="right" width="120">
<template #default="{ row }">
{{ formatMoney(row.shippedAmount) }}
</template>
</el-table-column>
<el-table-column label="已收款金额" prop="receivedAmount" align="right" width="120">
<template #default="{ row }">
<span class="text-success">{{ formatMoney(row.receivedAmount) }}</span>
</template>
</el-table-column>
<el-table-column label="应收金额" prop="receivableAmount" align="right" width="120">
<template #default="{ row }">
<span :class="{ 'text-danger': row.receivableAmount > 0 }">
{{ formatMoney(row.receivableAmount) }}
</span>
</template>
</el-table-column>
<el-table-column label="发货进度" width="150">
<template #default="{ row }">
<el-progress
:percentage="calcPercent(row.shippedQuantity, row.contractQuantity)"
:stroke-width="10"
/>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-show="total > 0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
layout="total, prev, pager, next"
@pagination="getList"
/>
</div>
</template>
<script>
export default {
name: 'ProductTable',
props: {
customerId: {
type: Number,
required: true
}
},
data() {
return {
loading: false,
list: [],
total: 0,
queryParams: {
pageNum: 1,
pageSize: 10,
customerId: null,
salesLedgerId: null
},
filterData: {
salesLedgerId: null
},
contractList: []
}
},
watch: {
customerId(val) {
if (val) {
this.queryParams.customerId = val
this.getList()
this.getContractList()
}
}
},
methods: {
getList() {
this.loading = true
this.queryParams.salesLedgerId = this.filterData.salesLedgerId
this.$axios.get('/metricStatistics/customerTransactionsProducts', { params: this.queryParams })
.then(res => {
this.list = res.data.records
this.total = res.data.total
})
.finally(() => {
this.loading = false
})
},
getContractList() {
// 获取该客户的合同列表用于筛选
this.$axios.get('/metricStatistics/customewTransactionsDetails', {
params: { customerId: this.customerId, pageNum: 1, pageSize: 100 }
}).then(res => {
this.contractList = res.data.records
})
},
formatMoney(value) {
if (!value) return '0.00'
return Number(value).toFixed(2)
},
calcPercent(shipped, total) {
if (!total || total === 0) return 0
return Math.round((shipped / total) * 100)
}
}
}
</script>
4. 发货明细组件 (ShipmentTable.vue)
<template>
<div>
<!-- 筛选 -->
<el-form :inline="true" size="small">
<el-form-item label="合同号">
<el-select v-model="filterData.salesLedgerId" clearable placeholder="全部合同" @change="getList">
<el-option v-for="item in contractList" :key="item.id" :label="item.salesContractNo" :value="item.id" />
</el-select>
</el-form-item>
</el-form>
<!-- 表格 -->
<el-table :data="list" v-loading="loading" size="small">
<el-table-column label="合同号" prop="salesContractNo" width="150" />
<el-table-column label="发货单号" prop="shippingNo" width="150" />
<el-table-column label="产品名称" prop="productName" />
<el-table-column label="规格型号" prop="model" width="120" />
<el-table-column label="发货数量" prop="shippingQuantity" align="right" width="100" />
<el-table-column label="发货金额" prop="shippingAmount" align="right" width="120">
<template #default="{ row }">
{{ formatMoney(row.shippingAmount) }}
</template>
</el-table-column>
<el-table-column label="出库批号" prop="batchNo" width="150" />
<el-table-column label="发货日期" prop="shippingDate" width="120" />
<el-table-column label="审批状态" prop="approvalStatus" width="100">
<template #default="{ row }">
<el-tag :type="row.approvalStatus === 1 ? 'success' : 'warning'" size="mini">
{{ row.approvalStatus === 1 ? '已审' : '待审' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="已收款金额" prop="receivedAmount" align="right" width="120">
<template #default="{ row }">
<span class="text-success">{{ formatMoney(row.receivedAmount) }}</span>
</template>
</el-table-column>
<el-table-column label="应收金额" prop="receivableAmount" align="right" width="120">
<template #default="{ row }">
<span :class="{ 'text-danger': row.receivableAmount > 0 }">
{{ formatMoney(row.receivableAmount) }}
</span>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-show="total > 0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
layout="total, prev, pager, next"
@pagination="getList"
/>
</div>
</template>
<script>
export default {
name: 'ShipmentTable',
props: {
customerId: {
type: Number,
required: true
}
},
data() {
return {
loading: false,
list: [],
total: 0,
queryParams: {
pageNum: 1,
pageSize: 10,
customerId: null,
salesLedgerId: null
},
filterData: {
salesLedgerId: null
},
contractList: []
}
},
watch: {
customerId(val) {
if (val) {
this.queryParams.customerId = val
this.getList()
this.getContractList()
}
}
},
methods: {
getList() {
this.loading = true
this.queryParams.salesLedgerId = this.filterData.salesLedgerId
this.$axios.get('/metricStatistics/customerTransactionsShipments', { params: this.queryParams })
.then(res => {
this.list = res.data.records
this.total = res.data.total
})
.finally(() => {
this.loading = false
})
},
getContractList() {
this.$axios.get('/metricStatistics/customewTransactionsDetails', {
params: { customerId: this.customerId, pageNum: 1, pageSize: 100 }
}).then(res => {
this.contractList = res.data.records
})
},
formatMoney(value) {
if (!value) return '0.00'
return Number(value).toFixed(2)
}
}
}
</script>
注意事项
- 路由配置:需在路由中新增客户往来详情页路由
/sales/customerTransactions/detail
- 组件拆分:产品明细和发货明细建议拆分为独立组件,便于复用和维护
- 筛选联动:产品明细和发货明细支持按合同筛选,合同列表从原有接口获取
- 数据格式:金额字段需统一使用
formatMoney 方法格式化显示
- 进度条显示:产品明细中的发货进度使用
el-progress 组件直观展示
- 状态标识:应收金额大于0时使用红色标识,已收款使用绿色标识
- 审批状态:发货明细中的审批状态使用
el-tag 展示,已审为绿色,待审为黄色
数据对比
优化前 vs 优化后
| 维度 |
优化前 |
优化后 |
| 客户往来 |
只有合同金额、收款、应收 |
新增合同数、产品数、发货率、收款率等12项指标 |
| 明细维度 |
仅合同明细 |
新增产品明细、发货明细 |
| 筛选能力 |
仅按客户名筛选 |
支持按合同筛选产品/发货明细 |
| 数据追溯 |
无法追溯具体发货 |
可追溯每条发货记录的收款情况 |
| 进度展示 |
无 |
发货进度条直观展示 |