| | |
| | | <div class="card-body"> |
| | | <div class="card-left"> |
| | | <div class="card-title">月度收入</div> |
| | | <div class="card-amount">{{ income.amount }}</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"> |
| | |
| | | </div> |
| | | <div class="metric-row"> |
| | | <span class="metric-label">逾期数</span> |
| | | <span class="metric-value metric-up">{{ income.overdueCount }}</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> |
| | |
| | | <div class="card-body"> |
| | | <div class="card-left"> |
| | | <div class="card-title">月度支出</div> |
| | | <div class="card-amount">{{ expense.amount }}</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 metric-down">{{ expense.netProfit }}</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">{{ expense.grossProfit }}</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> |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref } from 'vue' |
| | | import { onMounted, ref } from 'vue' |
| | | import { getMonthlyIncome, getMonthlyExpenditure } from '@/api/viewIndex' |
| | | |
| | | // 暂时使用本地示例数据,后续可接真实接口覆盖 |
| | | const income = ref({ |
| | | amount: 102, |
| | | repayRate: { value: 52, trend: 1 }, // 正向 ↑ |
| | | overdueCount: 10092, |
| | | overdueRate: { value: 12, trend: 1 }, |
| | | amount: 0, |
| | | repayRate: { value: 0, trend: 0 }, |
| | | overdueCount: 0, |
| | | overdueRate: { value: 0, trend: 0 }, |
| | | }) |
| | | |
| | | const expense = ref({ |
| | | amount: 102, |
| | | netProfit: 291013, |
| | | grossProfit: 10092, |
| | | profitRate: { value: 12, trend: -1 }, // 负向 ↓ |
| | | 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 `${num.toFixed(2)}%` |
| | | // 百分比展示始终用绝对值,小数保留两位 |
| | | return `${Math.abs(num).toFixed(2)}%` |
| | | } |
| | | |
| | | const metricClass = (metric) => |
| | | Number(metric.trend) >= 0 ? 'metric-up' : 'metric-down' |
| | | 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) => |
| | | Number(metric.trend) >= 0 ? '↑' : '↓' |
| | | const metricArrow = (metric) => { |
| | | if (metric?.trend === undefined || metric?.trend === null) return '' |
| | | return Number(metric.trend) >= 0 ? '↑' : '↓' |
| | | } |
| | | |
| | | onMounted(() => { |
| | | fetchMonthlyIncome() |
| | | fetchMonthlyExpenditure() |
| | | }) |
| | | </script> |
| | | |
| | | <style scoped> |
| | |
| | | flex: 1; |
| | | display: flex; |
| | | align-items: center; |
| | | padding: 18px 24px; |
| | | padding: 18px 10px; |
| | | background-image: url('@/assets/BI/border@2x.png'); |
| | | background-size: 100% 100%; |
| | | background-position: center; |
| | |
| | | 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 { |
| | |
| | | align-items: center; |
| | | } |
| | | |
| | | .metric-unit { |
| | | font-size: 12px; |
| | | margin-left: 2px; |
| | | } |
| | | |
| | | .metric-value .arrow { |
| | | font-size: 13px; |
| | | margin-left: 4px; |