gaoluyang
9 小时以前 e449a5408265e4bd1f6c66f5be28a42efac444ee
src/pages/sales/receiptPaymentLedger/index.vue
@@ -1,167 +1,261 @@
<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 class="search-text"
                    placeholder="请输入客户名称"
                    v-model="searchForm.searchText"
                    @change="searchChange"
                    clearable />
          <up-input v-model="query.projectName"
                    class="search-text"
                    placeholder="请输入项目名称搜索"
                    clearable
                    @change="getList" />
        </view>
        <view class="search-button"
              @click="handleQuery">
          <up-icon name="search"
        <view class="filter-button"
              @click="showFilter = true">
          <up-icon name="list"
                   size="24"
                   color="#999"></up-icon>
                   color="#999" />
        </view>
      </view>
    </view>
    <!-- 客户列表 -->
    <view class="customer-list-container">
      <view class="customer-list"
            v-if="tableData.length > 0">
        <view v-for="(item, index) in tableData"
              :key="item.id"
              class="customer-item"
              @click="rowClickMethod(item)">
          <view class="item-header">
            <view class="item-left">
              <view class="customer-icon">
                <up-icon name="file-text"
                         size="16"
                         color="#ffffff"></up-icon>
              </view>
              <text class="customer-name">{{ item.customerName }}</text>
            </view>
            <view class="item-right">
              <up-icon name="arrow-right"
    <!-- 项目列表 -->
    <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="#999"></up-icon>
                       color="#ffffff" />
            </view>
            <text class="item-id">{{ item.projectName || "-" }}</text>
          </view>
          <up-divider></up-divider>
          <view class="item-details">
            <view class="detail-row">
              <text class="detail-label">开票金额(元)</text>
              <text class="detail-value">{{ formattedNumber(item.invoiceTotal) }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">回款金额(元)</text>
              <text class="detail-value">{{ formattedNumber(item.receiptPaymentAmount) }}</text>
            </view>
            <view class="detail-row">
              <text class="detail-label">应收金额(元)</text>
              <text class="detail-value highlight danger">{{ formattedNumber(item.unReceiptPaymentAmount) }}</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>
      <view v-else
            class="no-data">
        <text>暂无客户数据</text>
        <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 { onMounted, ref, reactive, toRefs } from "vue";
  import { reactive, ref, onMounted } from "vue";
  import { onShow } from "@dcloudio/uni-app";
  import { invoiceLedgerSalesAccount } from "@/api/salesManagement/invoiceLedger";
  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 tableData = ref([]);
  const projectList = ref([]);
  const summary = ref({});
  const showFilter = ref(false);
  const page = reactive({
    current: -1,
    size: -1,
  const query = reactive({
    projectName: "",
    customerName: "",
    contractName: "",
  });
  const data = reactive({
    searchForm: {
      searchText: "",
      invoiceDate: "",
    },
  });
  const filterFields = [
    { prop: "customerName", label: "客户名称", type: "input", placeholder: "请输入客户名称" },
    { prop: "contractName", label: "合同名称", type: "input", placeholder: "请输入合同名称" },
  ];
  const { searchForm } = toRefs(data);
  // 返回上一页
  const goBack = () => {
    uni.navigateBack();
  };
  // 搜索框变化时触发
  const searchChange = val => {
    searchForm.value.searchText = val;
    getList();
  };
  // 查询列表
  const handleQuery = () => {
    getList();
  };
  const getList = () => {
    showLoadingToast("加载中...");
    invoiceLedgerSalesAccount({ ...searchForm.value, ...page })
    uni.showLoading({ title: "加载中...", mask: true });
    const params = { ...query, current: -1, size: -1 };
    projectTransactions(params)
      .then(res => {
        tableData.value = res.data.records;
        closeToast();
        projectList.value = unwrapList(res);
        uni.hideLoading();
      })
      .catch(() => {
        closeToast();
        uni.showToast({
          title: "查询失败",
          icon: "error",
        });
        uni.hideLoading();
        uni.showToast({ title: "查询失败", icon: "none" });
      });
  };
  const formattedNumber = value => {
    return parseFloat(value || 0).toFixed(2);
  const getSummary = () => {
    transactionsSummary()
      .then(res => {
        summary.value = res.data || {};
      })
      .catch(() => {});
  };
  // 显示加载提示
  const showLoadingToast = message => {
    uni.showLoading({
      title: message,
      mask: true,
    });
  const onFilterSearch = values => {
    Object.assign(query, values || {});
    getList();
  };
  // 关闭提示
  const closeToast = () => {
    uni.hideLoading();
  };
  const rowClickMethod = row => {
    // 使用 uni.setStorageSync 存储客户信息
    uni.setStorageSync("customerId", row.id);
    // 跳转到回款记录明细页面
  const goDetail = item => {
    if (!item?.projectId) return;
    uni.navigateTo({
      url: "/pages/sales/receiptPaymentLedger/detail",
      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; // 与公共样式中的 #ee0a24 不同
    color: #ff4757;
    font-weight: 500;
  }
</style>
</style>