gaoluyang
2026-08-06 377c0a70c094d024aeb0bb94f4b791c6d5530c1e
src/views/reportAnalysis/financialAnalysis/components/center-top.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,327 @@
<template>
  <div>
    <!-- é¡¶éƒ¨æ”¶æ”¯å¡ç‰‡ -->
    <div class="finance-cards">
      <!-- æœˆåº¦æ”¶å…¥ -->
      <div class="finance-card income-card">
        <div class="icon-box">
          <img src="@/assets/BI/icon@2x.png" alt="图标" class="card-icon" />
        </div>
        <div class="card-body">
          <div class="card-left">
            <div class="card-title">月度收入</div>
            <div class="card-amount">
              <span>{{ formatAmountWanNumber(income.amount) }}</span>
              <span v-if="isWanAmount(income.amount)" class="card-amount-unit">万</span>
            </div>
          </div>
          <div class="card-right">
            <div class="metric-row">
              <span class="metric-label">回款率</span>
              <span class="metric-value" :class="metricClass(income.repayRate)">
                {{ formatPercent(income.repayRate.value) }}
                <span class="arrow">{{ metricArrow(income.repayRate) }}</span>
              </span>
            </div>
            <div class="metric-row">
              <span class="metric-label">逾期数</span>
              <span class="metric-value metric-up">
                {{ formatAmountWanNumber(income.overdueCount) }}
                <span
                  v-if="isWanAmount(income.overdueCount)"
                  class="metric-unit"
                >
                  ä¸‡
                </span>
              </span>
            </div>
            <div class="metric-row">
              <span class="metric-label">逾期率</span>
              <span class="metric-value" :class="metricClass(income.overdueRate)">
                {{ formatPercent(income.overdueRate.value) }}
                <span class="arrow">{{ metricArrow(income.overdueRate) }}</span>
              </span>
            </div>
          </div>
        </div>
      </div>
      <!-- æœˆåº¦æ”¯å‡º -->
      <div class="finance-card expense-card">
        <div class="icon-box">
          <img src="@/assets/BI/icon@2x.png" alt="图标" class="card-icon" />
        </div>
        <div class="card-body">
          <div class="card-left">
            <div class="card-title">月度支出</div>
            <div class="card-amount">
              <span>{{ formatAmountWanNumber(expense.amount) }}</span>
              <span v-if="isWanAmount(expense.amount)" class="card-amount-unit">万</span>
            </div>
          </div>
          <div class="card-right">
            <div class="metric-row">
              <span class="metric-label">付款率</span>
              <span class="metric-value" :class="metricClass(expense.netProfit)">
                {{ formatPercent(expense.netProfit.value) }}
                <span class="arrow">{{ metricArrow(expense.netProfit) }}</span>
              </span>
            </div>
            <div class="metric-row">
              <span class="metric-label">毛利润</span>
              <span class="metric-value metric-down">
                {{ formatAmountWanNumber(expense.grossProfit) }}
                <span
                  v-if="isWanAmount(expense.grossProfit)"
                  class="metric-unit"
                >
                  ä¸‡
                </span>
              </span>
            </div>
            <div class="metric-row">
              <span class="metric-label">利润率</span>
              <span class="metric-value" :class="metricClass(expense.profitRate)">
                {{ formatPercent(expense.profitRate.value) }}
                <span class="arrow">{{ metricArrow(expense.profitRate) }}</span>
              </span>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import { getMonthlyIncome, getMonthlyExpenditure } from '@/api/viewIndex'
const income = ref({
  amount: 0,
  repayRate: { value: 0, trend: 0 },
  overdueCount: 0,
  overdueRate: { value: 0, trend: 0 },
})
const expense = ref({
  amount: 0,
  netProfit: { value: 0, trend: 0 },
  grossProfit: 0,
  profitRate: { value: 0, trend: 0 },
})
const toNumber = (val) => {
  const num = Number(val)
  return Number.isFinite(num) ? num : 0
}
const fetchMonthlyIncome = async () => {
  try {
    const res = await getMonthlyIncome()
    const data = res?.data || {}
    income.value.amount = toNumber(data.monthlyIncome)
    const collectionRate = toNumber(data.collectionRate)
    const overdueRate = toNumber(data.overdueRate)
    income.value.repayRate = {
      value: collectionRate,
      trend: collectionRate >= 0 ? 1 : -1,
    }
    income.value.overdueCount = toNumber(data.overdueNum)
    income.value.overdueRate = {
      value: overdueRate,
      trend: overdueRate >= 0 ? 1 : -1,
    }
  } catch {
    income.value.amount = 0
    income.value.repayRate = { value: 0, trend: 0 }
    income.value.overdueCount = 0
    income.value.overdueRate = { value: 0, trend: 0 }
  }
}
const fetchMonthlyExpenditure = async () => {
  try {
    const res = await getMonthlyExpenditure()
    const data = res?.data || {}
    expense.value.amount = toNumber(data.monthlyExpenditure)
    const paymentRate = toNumber(data.paymentRate)
    expense.value.netProfit = {
      value: paymentRate,
      trend: paymentRate >= 0 ? 1 : -1,
    }
    expense.value.grossProfit = toNumber(data.grossProfit)
    const profitMarginRate = toNumber(data.profitMarginRate)
    expense.value.profitRate = {
      value: profitMarginRate,
      trend: profitMarginRate >= 0 ? 1 : -1,
    }
  } catch {
    expense.value.amount = 0
    expense.value.netProfit = { value: 0, trend: 0 }
    expense.value.grossProfit = 0
    expense.value.profitRate = { value: 0, trend: 0 }
  }
}
const isWanAmount = (val) => {
  const num = toNumber(val)
  return Math.abs(num) >= 10000
}
const formatAmountWanNumber = (val) => {
  const num = toNumber(val)
  if (Math.abs(num) >= 10000) {
    return (num / 10000).toFixed(2)
  }
  return num.toFixed(2)
}
const formatPercent = (val) => {
  const num = toNumber(val)
  // ç™¾åˆ†æ¯”展示始终用绝对值,小数保留两位
  return `${Math.abs(num).toFixed(2)}%`
}
const metricClass = (metric) => {
  if (metric?.trend === undefined || metric?.trend === null) return 'metric-up'
  return Number(metric.trend) >= 0 ? 'metric-up' : 'metric-down'
}
const metricArrow = (metric) => {
  if (metric?.trend === undefined || metric?.trend === null) return ''
  return Number(metric.trend) >= 0 ? '↑' : '↓'
}
onMounted(() => {
  fetchMonthlyIncome()
  fetchMonthlyExpenditure()
})
</script>
<style scoped>
.finance-cards {
  display: flex;
  justify-content: space-between;
  gap: 16px;
}
.finance-card {
  flex: 1;
  display: flex;
  align-items: center;
  padding: 18px 10px;
  background-image: url('@/assets/BI/border@2x.png');
  background-size: 100% 100%;
  background-position: center;
  background-repeat: no-repeat;
  min-height: 138px;
}
.icon-box {
  width: 92px;
  height: 92px;
  /* border: 1px dashed rgba(208, 231, 255, 0.55); */
  display: flex;
  align-items: center;
  justify-content: center;
  margin-right: 18px;
}
.card-icon {
  width: 78px;
  height: 78px;
}
.card-body {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 18px;
}
.card-left {
  min-width: 90px;
}
.card-title {
  font-weight: 400;
  font-size: 18px;
  color: rgba(208, 231, 255, 0.7);
}
.card-amount {
  font-weight: 500;
  font-size: 36px;
  line-height: 1.1;
  margin-top: 8px;
  display: inline-flex;
  align-items: baseline;
  white-space: nowrap;
  background: linear-gradient(360deg, #008bfd 0%, #ffffff 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}
.card-amount-unit {
  font-size: 20px;
  margin-left: 4px;
}
.card-right {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding-right: 6px;
}
.metric-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: 14px;
  color: #d0e7ff;
  white-space: nowrap;
}
.metric-label {
  margin-right: 12px;
}
.metric-label {
  opacity: 0.8;
}
.metric-value {
  font-weight: 600;
  display: inline-flex;
  align-items: center;
}
.metric-unit {
  font-size: 12px;
  margin-left: 2px;
}
.metric-value .arrow {
  font-size: 13px;
  margin-left: 4px;
}
.metric-up {
  color: #00c853;
}
.metric-down {
  color: #ff5252;
}
</style>