| | |
| | | <template> |
| | | <view class="receipt-payment-detail"> |
| | | <!-- 使用通用页面头部组件 --> |
| | | <PageHeader title="客户往来详情" |
| | | <PageHeader :title="pageTitle" |
| | | @back="goBack" /> |
| | | <!-- 统计信息 --> |
| | | <view class="summary-info" |
| | | v-if="tableData.length > 0"> |
| | | <view class="summary-item"> |
| | | <text class="summary-label">总记录数</text> |
| | | <text class="summary-value">{{ tableData.length }}</text> |
| | | |
| | | <!-- 第二级:项目下的客户往来 --> |
| | | <template v-if="level === 'customer'"> |
| | | <!-- 项目应收摘要(需求七:应收账款管理重点信息摘要) --> |
| | | <view class="summary-info"> |
| | | <view class="summary-item"> |
| | | <text class="summary-label">客户数</text> |
| | | <text class="summary-value">{{ customerTotal }}</text> |
| | | </view> |
| | | <view class="summary-item"> |
| | | <text class="summary-label">签约总额</text> |
| | | <text class="summary-value">{{ fmtMoney(sumOf("contractAmounts")) }}</text> |
| | | </view> |
| | | <view class="summary-item"> |
| | | <text class="summary-label">已回款</text> |
| | | <text class="summary-value highlight">{{ fmtMoney(sumOf("receiptPaymentAmount")) }}</text> |
| | | </view> |
| | | <view class="summary-item"> |
| | | <text class="summary-label">应收总额</text> |
| | | <text class="summary-value danger">{{ fmtMoney(sumOf("receiptableAmount")) }}</text> |
| | | </view> |
| | | <view class="summary-item"> |
| | | <text class="summary-label">超期合同</text> |
| | | <text class="summary-value danger">{{ overdueCount }} 份</text> |
| | | </view> |
| | | </view> |
| | | <view class="summary-item"> |
| | | <text class="summary-label">开票总金额</text> |
| | | <text class="summary-value">{{ formatAmount(invoiceTotal) }}</text> |
| | | </view> |
| | | <view class="summary-item"> |
| | | <text class="summary-label">回款总金额</text> |
| | | <text class="summary-value highlight">{{ formatAmount(receiptTotal) }}</text> |
| | | </view> |
| | | <view class="summary-item"> |
| | | <text class="summary-label">应收总金额</text> |
| | | <text class="summary-value danger">{{ formatAmount(unReceiptTotal) }}</text> |
| | | </view> |
| | | </view> |
| | | <!-- 回款记录明细列表 --> |
| | | <view class="detail-list" |
| | | v-if="tableData.length > 0"> |
| | | <view v-for="(item, index) in tableData" |
| | | :key="index" |
| | | class="detail-item"> |
| | | <view class="item-header"> |
| | | <view class="item-left"> |
| | | <view class="record-icon"> |
| | | <up-icon name="file-text" |
| | | size="16" |
| | | color="#ffffff"></up-icon> |
| | | |
| | | <view class="detail-list" |
| | | v-if="customerList.length > 0"> |
| | | <view v-for="item in customerList" |
| | | :key="item.customerId" |
| | | class="detail-item" |
| | | @click="goContracts(item)"> |
| | | <view class="item-header"> |
| | | <view class="item-left"> |
| | | <view class="record-icon"> |
| | | <up-icon name="account" |
| | | size="16" |
| | | color="#ffffff" /> |
| | | </view> |
| | | <text class="item-index">{{ item.customerName || "未命名客户" }}</text> |
| | | </view> |
| | | <text class="item-index">{{ index + 1 }}</text> |
| | | <up-tag :text="item.overdueCount > 0 ? `${item.overdueCount} 份超期` : '正常履约'" |
| | | :type="item.overdueCount > 0 ? 'error' : 'success'" |
| | | size="mini" |
| | | shape="circle" /> |
| | | </view> |
| | | <view class="item-date">{{ item.happenTime }}</view> |
| | | </view> |
| | | <up-divider></up-divider> |
| | | <view class="item-details"> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">开票金额(元)</text> |
| | | <text class="detail-value">{{ formatAmount(item.invoiceTotal) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">回款金额(元)</text> |
| | | <text class="detail-value highlight">{{ formatAmount(item.receiptPaymentAmount) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">应收金额(元)</text> |
| | | <text class="detail-value danger">{{ formatAmount(item.unReceiptPaymentAmount) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">发生日期</text> |
| | | <text class="detail-value">{{ item.receiptPaymentDate }}</text> |
| | | <up-divider></up-divider> |
| | | <view class="item-details"> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">签约总额</text> |
| | | <text class="detail-value">{{ fmtMoney(item.contractAmounts) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">已回款</text> |
| | | <text class="detail-value highlight">{{ fmtMoney(item.receiptPaymentAmount) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">应收金额</text> |
| | | <text class="detail-value danger">{{ fmtMoney(item.receiptableAmount) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">退货金额</text> |
| | | <text class="detail-value">{{ fmtMoney(item.returnAmount) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">回款进度</text> |
| | | <text class="detail-value" |
| | | :class="{ danger: item.overdueCount > 0 }"> |
| | | {{ calcRate(item.receiptPaymentAmount, item.contractAmounts) }}% |
| | | </text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">合同数</text> |
| | | <text class="detail-value">{{ item.contractCount || 0 }} 份</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-else |
| | | class="no-data"> |
| | | <text>暂无回款记录</text> |
| | | </view> |
| | | <view v-else |
| | | class="no-data"> |
| | | <text>该项目下暂无客户往来数据</text> |
| | | </view> |
| | | </template> |
| | | |
| | | <!-- 第三级:客户下的合同明细 --> |
| | | <template v-else> |
| | | <view class="detail-list" |
| | | v-if="contractList.length > 0"> |
| | | <view v-for="(item, index) in contractList" |
| | | :key="index" |
| | | class="detail-item"> |
| | | <view class="item-header"> |
| | | <view class="item-left"> |
| | | <view class="record-icon"> |
| | | <up-icon name="file-text" |
| | | size="16" |
| | | color="#ffffff" /> |
| | | </view> |
| | | <text class="item-index">{{ item.contractName || item.salesContractNo || "未命名合同" }}</text> |
| | | </view> |
| | | <up-tag :text="contractStatusText(item)" |
| | | :type="contractStatusType(item)" |
| | | size="mini" |
| | | shape="circle" /> |
| | | </view> |
| | | <up-divider></up-divider> |
| | | <view class="item-details"> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">合同编号</text> |
| | | <text class="detail-value">{{ item.salesContractNo || "-" }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">合同总额</text> |
| | | <text class="detail-value">{{ fmtMoney(item.contractAmount) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">已回款</text> |
| | | <text class="detail-value" |
| | | :class="{ danger: item.isPaymentTimeout }"> |
| | | {{ fmtMoney(item.receiptPaymentAmount) }} |
| | | ({{ calcRate(item.receiptPaymentAmount, item.contractAmount) }}%) |
| | | </text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">应收金额</text> |
| | | <text class="detail-value danger">{{ fmtMoney(item.receiptableAmount) }}</text> |
| | | </view> |
| | | <view class="detail-row"> |
| | | <text class="detail-label">退货金额</text> |
| | | <text class="detail-value">{{ fmtMoney(item.returnAmount) }}</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <view v-else |
| | | class="no-data"> |
| | | <text>该客户下暂无合同明细</text> |
| | | </view> |
| | | </template> |
| | | </view> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, computed, onMounted } from "vue"; |
| | | import { onShow } from "@dcloudio/uni-app"; |
| | | import { customerInteractions } from "@/api/salesManagement/receiptPayment.js"; |
| | | import { computed, ref } from "vue"; |
| | | import { onLoad } from "@dcloudio/uni-app"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import { |
| | | customewTransactions, |
| | | customewTransactionsDetails, |
| | | } from "@/api/salesManagement/indicatorStats"; |
| | | import { unwrapList } from "@/utils/list"; |
| | | |
| | | // 客户信息 |
| | | // level: customer = 项目下的客户汇总;contract = 客户下的合同明细 |
| | | const level = ref("customer"); |
| | | const projectId = ref(""); |
| | | const projectName = ref(""); |
| | | const customerId = ref(""); |
| | | const customerName = ref(""); |
| | | |
| | | // 表格数据 |
| | | const tableData = ref([]); |
| | | const customerList = ref([]); |
| | | const contractList = ref([]); |
| | | const customerTotal = ref(0); |
| | | |
| | | const invoiceTotal = computed(() => { |
| | | return tableData.value.reduce((sum, item) => { |
| | | return sum + (parseFloat(item.invoiceTotal) || 0); |
| | | }, 0); |
| | | const pageTitle = computed(() => { |
| | | if (level.value === "customer") return projectName.value || "客户往来"; |
| | | return customerName.value || "合同明细"; |
| | | }); |
| | | |
| | | const receiptTotal = computed(() => { |
| | | return tableData.value.reduce((sum, item) => { |
| | | return sum + (parseFloat(item.receiptPaymentAmount) || 0); |
| | | }, 0); |
| | | }); |
| | | const sumOf = field => |
| | | customerList.value.reduce((sum, item) => sum + (Number(item[field]) || 0), 0); |
| | | |
| | | const unReceiptTotal = computed(() => { |
| | | return tableData.value.reduce((sum, item) => { |
| | | return sum + (parseFloat(item.unReceiptPaymentAmount) || 0); |
| | | }, 0); |
| | | }); |
| | | const overdueCount = computed(() => |
| | | customerList.value.reduce((sum, item) => sum + (Number(item.overdueCount) || 0), 0) |
| | | ); |
| | | |
| | | // 返回上一页 |
| | | const goBack = () => { |
| | | uni.removeStorageSync("customerId"); |
| | | uni.navigateBack(); |
| | | }; |
| | | |
| | | // 获取页面参数 |
| | | const getPageParams = () => { |
| | | // 从本地存储获取客户ID |
| | | const storedCustomerId = uni.getStorageSync("customerId"); |
| | | if (storedCustomerId) { |
| | | customerId.value = storedCustomerId; |
| | | } |
| | | }; |
| | | |
| | | // 查询列表 |
| | | const getList = () => { |
| | | if (!customerId.value) { |
| | | uni.showToast({ |
| | | title: "客户信息缺失", |
| | | icon: "error", |
| | | const loadCustomers = () => { |
| | | if (!projectId.value) return; |
| | | uni.showLoading({ title: "加载中...", mask: true }); |
| | | customewTransactions({ projectId: projectId.value, current: -1, size: -1 }) |
| | | .then(res => { |
| | | customerList.value = unwrapList(res); |
| | | customerTotal.value = Number(res?.data?.total ?? customerList.value.length); |
| | | uni.hideLoading(); |
| | | }) |
| | | .catch(() => { |
| | | uni.hideLoading(); |
| | | uni.showToast({ title: "查询失败", icon: "none" }); |
| | | }); |
| | | return; |
| | | } |
| | | }; |
| | | |
| | | showLoadingToast("加载中..."); |
| | | const param = { |
| | | const loadContracts = () => { |
| | | if (!projectId.value || !customerId.value) return; |
| | | uni.showLoading({ title: "加载中...", mask: true }); |
| | | customewTransactionsDetails({ |
| | | projectId: projectId.value, |
| | | customerId: customerId.value, |
| | | current: -1, |
| | | size: -1, |
| | | }; |
| | | |
| | | customerInteractions(param) |
| | | }) |
| | | .then(res => { |
| | | tableData.value = res.data; |
| | | closeToast(); |
| | | contractList.value = unwrapList(res); |
| | | uni.hideLoading(); |
| | | }) |
| | | .catch(() => { |
| | | closeToast(); |
| | | uni.showToast({ |
| | | title: "查询失败", |
| | | icon: "error", |
| | | }); |
| | | uni.hideLoading(); |
| | | uni.showToast({ title: "查询失败", icon: "none" }); |
| | | }); |
| | | }; |
| | | |
| | | // 格式化金额 |
| | | const formatAmount = amount => { |
| | | return amount ? parseFloat(amount).toFixed(2) : "0.00"; |
| | | const goContracts = item => { |
| | | customerId.value = item.customerId; |
| | | customerName.value = item.customerName || ""; |
| | | level.value = "contract"; |
| | | loadContracts(); |
| | | }; |
| | | |
| | | // 显示加载提示 |
| | | const showLoadingToast = message => { |
| | | uni.showLoading({ |
| | | title: message, |
| | | mask: true, |
| | | }); |
| | | // 返回:第三级退回第二级,第二级才真正退出页面 |
| | | const goBack = () => { |
| | | if (level.value === "contract") { |
| | | level.value = "customer"; |
| | | contractList.value = []; |
| | | return; |
| | | } |
| | | uni.navigateBack(); |
| | | }; |
| | | |
| | | // 关闭提示 |
| | | const closeToast = () => { |
| | | uni.hideLoading(); |
| | | const contractStatusText = item => { |
| | | if (item.isPaymentTimeout) return "超期未回"; |
| | | const amount = Number(item.contractAmount) || 0; |
| | | if (amount > 0 && (Number(item.receiptPaymentAmount) || 0) >= amount) return "已结清"; |
| | | return "履行中"; |
| | | }; |
| | | |
| | | onShow(() => { |
| | | // 页面显示时获取参数并刷新列表 |
| | | getPageParams(); |
| | | getList(); |
| | | }); |
| | | const contractStatusType = item => { |
| | | if (item.isPaymentTimeout) return "error"; |
| | | const amount = Number(item.contractAmount) || 0; |
| | | if (amount > 0 && (Number(item.receiptPaymentAmount) || 0) >= amount) return "success"; |
| | | return "primary"; |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | // 页面加载时获取参数并刷新列表 |
| | | getPageParams(); |
| | | getList(); |
| | | const fmtMoney = value => { |
| | | const num = Number(value); |
| | | if (!Number.isFinite(num)) return "¥ 0.00"; |
| | | return "¥ " + num.toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); |
| | | }; |
| | | |
| | | const calcRate = (paid, amount) => { |
| | | const total = Number(amount); |
| | | if (!Number.isFinite(total) || total === 0) return "0.0"; |
| | | return (((Number(paid) || 0) / total) * 100).toFixed(1); |
| | | }; |
| | | |
| | | onLoad(options => { |
| | | projectId.value = options?.projectId || ""; |
| | | projectName.value = options?.projectName ? decodeURIComponent(options.projectName) : ""; |
| | | if (!projectId.value) { |
| | | uni.showToast({ title: "缺少项目参数", icon: "none" }); |
| | | return; |
| | | } |
| | | loadCustomers(); |
| | | }); |
| | | </script> |
| | | |
| | |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | flex: 1; |
| | | min-width: 0; |
| | | } |
| | | |
| | | .record-icon { |
| | |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .item-index { |
| | | font-size: 14px; |
| | | color: #333; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .item-date { |
| | | font-size: 12px; |
| | | color: #666; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .item-details { |