<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 fetchMonthlyIncome = async () => {
|
const res = await getMonthlyIncome()
|
const data = res?.data || {}
|
|
income.value.amount = data.monthlyIncome ?? 0
|
const collectionRate = Number(data.collectionRate ?? 0)
|
const overdueRate = Number(data.overdueRate ?? 0)
|
income.value.repayRate = {
|
value: collectionRate,
|
trend: collectionRate >= 0 ? 1 : -1,
|
}
|
income.value.overdueCount = data.overdueNum ?? 0
|
income.value.overdueRate = {
|
value: overdueRate,
|
trend: overdueRate >= 0 ? 1 : -1,
|
}
|
}
|
|
const fetchMonthlyExpenditure = async () => {
|
const res = await getMonthlyExpenditure()
|
const data = res?.data || {}
|
|
expense.value.amount = data.monthlyExpenditure ?? 0
|
const paymentRate = Number(data.paymentRate ?? 0)
|
expense.value.netProfit = {
|
value: paymentRate,
|
trend: paymentRate >= 0 ? 1 : -1,
|
}
|
expense.value.grossProfit = data.grossProfit ?? 0
|
|
const profitMarginRate = Number(data.profitMarginRate ?? 0)
|
expense.value.profitRate = {
|
value: profitMarginRate,
|
trend: profitMarginRate >= 0 ? 1 : -1,
|
}
|
}
|
|
const isWanAmount = (val) => {
|
const num = Number(val) || 0
|
return Math.abs(num) >= 10000
|
}
|
|
const formatAmountWanNumber = (val) => {
|
const num = Number(val) || 0
|
if (Math.abs(num) >= 10000) {
|
return (num / 10000).toFixed(2)
|
}
|
return num.toFixed(2)
|
}
|
|
const formatPercent = (val) => {
|
const num = Number(val) || 0
|
// 百分比展示始终用绝对值,小数保留两位
|
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>
|