| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |