编辑 | blame | 历史 | 原始文档

客户往来多维度明细功能前端联调文档

优化客户往来功能,新增多维度明细查询接口,支持产品明细和发货明细维度

涉及页面

  • 营销管理 / 客户往来 - 客户往来列表页
  • 营销管理 / 客户往来 - 客户往来详情页(新增)
  • 营销管理 / 客户往来 - 产品明细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>

注意事项

  1. 路由配置:需在路由中新增客户往来详情页路由 /sales/customerTransactions/detail
  2. 组件拆分:产品明细和发货明细建议拆分为独立组件,便于复用和维护
  3. 筛选联动:产品明细和发货明细支持按合同筛选,合同列表从原有接口获取
  4. 数据格式:金额字段需统一使用 formatMoney 方法格式化显示
  5. 进度条显示:产品明细中的发货进度使用 el-progress 组件直观展示
  6. 状态标识:应收金额大于0时使用红色标识,已收款使用绿色标识
  7. 审批状态:发货明细中的审批状态使用 el-tag 展示,已审为绿色,待审为黄色

数据对比

优化前 vs 优化后

维度 优化前 优化后
客户往来 只有合同金额、收款、应收 新增合同数、产品数、发货率、收款率等12项指标
明细维度 仅合同明细 新增产品明细、发货明细
筛选能力 仅按客户名筛选 支持按合同筛选产品/发货明细
数据追溯 无法追溯具体发货 可追溯每条发货记录的收款情况
进度展示 发货进度条直观展示