<template>
|
<view class="sales-account">
|
<PageHeader title="客户往来"
|
@back="goBack" />
|
|
<!-- 统计条(需求七:应收摘要) -->
|
<view class="summary-strip">
|
<view class="summary-cell">
|
<text class="summary-num">{{ summary.totalProjects || 0 }}</text>
|
<text class="summary-cap">项目</text>
|
</view>
|
<view class="summary-cell">
|
<text class="summary-num">{{ summary.totalCustomers || 0 }}</text>
|
<text class="summary-cap">客户</text>
|
</view>
|
<view class="summary-cell">
|
<text class="summary-num">{{ summary.totalContracts || 0 }}</text>
|
<text class="summary-cap">合同</text>
|
</view>
|
<view class="summary-cell">
|
<text class="summary-num">{{ fmtMoney(summary.totalAmount) }}</text>
|
<text class="summary-cap">签约总额</text>
|
</view>
|
<view class="summary-cell overdue">
|
<text class="summary-num">{{ summary.totalOverdueCount || 0 }}</text>
|
<text class="summary-cap">超30天未回款</text>
|
</view>
|
</view>
|
|
<!-- 搜索区域 -->
|
<view class="search-section">
|
<view class="search-bar">
|
<view class="search-input">
|
<up-input v-model="query.projectName"
|
class="search-text"
|
placeholder="请输入项目名称搜索"
|
clearable
|
@change="getList" />
|
</view>
|
<view class="filter-button"
|
@click="showFilter = true">
|
<up-icon name="list"
|
size="24"
|
color="#999" />
|
</view>
|
</view>
|
</view>
|
|
<!-- 项目列表 -->
|
<view v-if="projectList.length > 0"
|
class="ledger-list">
|
<view v-for="item in projectList"
|
:key="item.projectId"
|
class="ledger-item"
|
@click="goDetail(item)">
|
<view class="item-header">
|
<view class="item-left">
|
<view class="document-icon">
|
<up-icon name="file-text"
|
size="16"
|
color="#ffffff" />
|
</view>
|
<text class="item-id">{{ item.projectName || "-" }}</text>
|
</view>
|
<up-tag :text="overdueText(item.overdueCount)"
|
:type="item.overdueCount > 0 ? 'error' : 'success'"
|
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.projectCode || "-" }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">签约总额</text>
|
<text class="detail-value highlight">{{ fmtMoney(item.contractAmounts) }}</text>
|
</view>
|
<view class="detail-row">
|
<text class="detail-label">开票金额</text>
|
<text class="detail-value">{{ fmtMoney(item.invoiceAmount) }}</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.customerCount || 0 }} 家 / {{ item.contractCount || 0 }} 份</text>
|
</view>
|
</view>
|
<view class="action-buttons">
|
<up-button class="action-btn"
|
size="small"
|
type="primary"
|
@click.stop="goDetail(item)">查看客户往来</up-button>
|
</view>
|
</view>
|
</view>
|
<view v-else
|
class="no-data">
|
<text>暂无项目往来数据</text>
|
</view>
|
|
<FilterPanel v-model:show="showFilter"
|
:model-value="query"
|
title="客户往来筛选"
|
:fields="filterFields"
|
@search="onFilterSearch"
|
@reset="onFilterSearch" />
|
</view>
|
</template>
|
|
<script setup>
|
import { reactive, ref, onMounted } from "vue";
|
import { onShow } from "@dcloudio/uni-app";
|
import PageHeader from "@/components/PageHeader.vue";
|
import FilterPanel from "@/components/FilterPanel.vue";
|
import {
|
projectTransactions,
|
transactionsSummary,
|
} from "@/api/salesManagement/indicatorStats";
|
import { unwrapList } from "@/utils/list";
|
|
const projectList = ref([]);
|
const summary = ref({});
|
const showFilter = ref(false);
|
|
const query = reactive({
|
projectName: "",
|
customerName: "",
|
contractName: "",
|
});
|
|
const filterFields = [
|
{ prop: "customerName", label: "客户名称", type: "input", placeholder: "请输入客户名称" },
|
{ prop: "contractName", label: "合同名称", type: "input", placeholder: "请输入合同名称" },
|
];
|
|
const goBack = () => {
|
uni.navigateBack();
|
};
|
|
const getList = () => {
|
uni.showLoading({ title: "加载中...", mask: true });
|
const params = { ...query, current: -1, size: -1 };
|
projectTransactions(params)
|
.then(res => {
|
projectList.value = unwrapList(res);
|
uni.hideLoading();
|
})
|
.catch(() => {
|
uni.hideLoading();
|
uni.showToast({ title: "查询失败", icon: "none" });
|
});
|
};
|
|
const getSummary = () => {
|
transactionsSummary()
|
.then(res => {
|
summary.value = res.data || {};
|
})
|
.catch(() => {});
|
};
|
|
const onFilterSearch = values => {
|
Object.assign(query, values || {});
|
getList();
|
};
|
|
const goDetail = item => {
|
if (!item?.projectId) return;
|
uni.navigateTo({
|
url: `/pages/sales/receiptPaymentLedger/detail?projectId=${item.projectId}`
|
+ `&projectName=${encodeURIComponent(item.projectName || "")}`,
|
});
|
};
|
|
const overdueText = count => (count > 0 ? `${count} 份超期` : "正常履约");
|
|
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);
|
};
|
|
onShow(() => {
|
getList();
|
getSummary();
|
});
|
|
onMounted(() => {
|
getList();
|
getSummary();
|
});
|
</script>
|
|
<style scoped lang="scss">
|
@import "@/styles/sales-common.scss";
|
|
.summary-strip {
|
display: flex;
|
flex-wrap: wrap;
|
background: #ffffff;
|
margin: 12px 12px 0;
|
border-radius: 12px;
|
padding: 12px 8px;
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
}
|
|
.summary-cell {
|
flex: 1 0 33%;
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
padding: 6px 0;
|
}
|
|
.summary-num {
|
font-size: 16px;
|
font-weight: 600;
|
color: #333333;
|
}
|
|
.summary-cap {
|
font-size: 11px;
|
color: #909399;
|
margin-top: 2px;
|
}
|
|
.summary-cell.overdue .summary-num {
|
color: #ff4757;
|
}
|
|
.detail-value.danger {
|
color: #ff4757;
|
font-weight: 500;
|
}
|
</style>
|