354 lines
11 KiB
Vue
354 lines
11 KiB
Vue
<template>
|
||
<view class="page_padding">
|
||
<!-- 全部项目 -->
|
||
<view class="filter-section">
|
||
<view class="filter-row-full">
|
||
<picker mode="selector" :range="projectOptions" range-key="label" @change="onProjectChange">
|
||
<view class="filter-label">
|
||
<text>{{ selectedProjectLabel || '全部项目' }}</text>
|
||
<text class="filter-arrow">▼</text>
|
||
</view>
|
||
</picker>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 统计卡片 -->
|
||
<view class="overview-card">
|
||
<view class="card-left">
|
||
<text class="card-num">{{ manageArea }}</text>
|
||
<text class="card-label">管理面积(m²)</text>
|
||
</view>
|
||
<view class="card-right">
|
||
<text class="card-num">{{ serviceHouseholds }}</text>
|
||
<text class="card-label">服务户数(户)</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 入住率 / 出租率 -->
|
||
<view class="rate-row">
|
||
<view class="rate-item rate-occupy">
|
||
<text class="rate-text">入住率</text>
|
||
<text class="rate-num">{{ occupancyRate }}%</text>
|
||
</view>
|
||
<view class="rate-item rate-rental">
|
||
<text class="rate-text">出租率</text>
|
||
<text class="rate-num">{{ rentalRate }}%</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 收费总览 -->
|
||
<FeeOverviewCard
|
||
:fee-type-options="feeTypeOptions"
|
||
:selected-fee-type-label="selectedFeeTypeLabel"
|
||
:active-tab="activeTab"
|
||
:arcbar-percent="arcbarPercent"
|
||
:metric-list="metricList"
|
||
:chart-loading="feeLoading"
|
||
@tab-change="switchTab"
|
||
@fee-type-change="onFeeTypeChange"
|
||
/>
|
||
|
||
<!-- 车位收费总览 -->
|
||
<ParkingFeeCard
|
||
:parking-count="parkingCount"
|
||
:parking-year-receivable="parkingYearReceivable"
|
||
:parking-year-received="parkingYearReceived"
|
||
:parking-arcbar-percent="parkingArcbarPercent"
|
||
/>
|
||
|
||
<!-- 押金余额统计 -->
|
||
<DepositCard
|
||
:deposit-balance="depositBalance"
|
||
:deposit-items="depositItems"
|
||
/>
|
||
|
||
<!-- 巡检总览 -->
|
||
<InspectionOverview
|
||
:inspection-list="inspectionList"
|
||
/>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref } from 'vue'
|
||
import { onShow, onPullDownRefresh } from '@dcloudio/uni-app'
|
||
import { getVillageList, getFeeTypes } from '@/api/api.js'
|
||
import FeeOverviewCard from './components/FeeOverviewCard.vue'
|
||
import ParkingFeeCard from './components/ParkingFeeCard.vue'
|
||
import DepositCard from './components/DepositCard.vue'
|
||
import InspectionOverview from './components/InspectionOverview.vue'
|
||
import { getManagedArea, getRate, getFeeSummary, getParkingFee, getDepositBalance, getInspection } from '@/api/statisticsApi.js'
|
||
|
||
|
||
|
||
const manageArea = ref(0)
|
||
const serviceHouseholds = ref(0)
|
||
const occupancyRate = ref(100)
|
||
const rentalRate = ref(0)
|
||
const activeTab = ref('day')
|
||
|
||
const feeTypeOptions = ref([])
|
||
const selectedFeeType = ref('')
|
||
const selectedFeeTypeLabel = ref('全部收费类型')
|
||
const projectOptions = ref([])
|
||
const selectedProject = ref('')
|
||
const selectedProjectLabel = ref('全部项目')
|
||
|
||
const feeLoading = ref(false)
|
||
const arcbarPercent = ref(0)
|
||
|
||
const metricList = ref([
|
||
{ label: '应收总额', value: '0.00', yoy: '-', mom: '-' },
|
||
{ label: '实收总额', value: '0.00', yoy: '-', mom: '-' },
|
||
{ label: '欠收总额', value: '0.00', yoy: '-', mom: '-' }
|
||
])
|
||
|
||
// 车位收费总览
|
||
const parkingCount = ref(0)
|
||
const parkingYearReceivable = ref(0)
|
||
const parkingYearReceived = ref(0)
|
||
const parkingArcbarPercent = ref(0)
|
||
|
||
// 押金余额统计
|
||
const depositBalance = ref('0')
|
||
const depositItems = ref([])
|
||
|
||
// 巡检总览
|
||
const inspectionList = ref([
|
||
{ label: '巡检任务', value: 0, icon: 'https://wuye.ckdzkj.com/images/imgs/inspection-task.png' },
|
||
{ label: '已完成', value: 0, icon: 'https://wuye.ckdzkj.com/images/imgs/inspection-done.png' },
|
||
{ label: '已过期', value: 0, icon: 'https://wuye.ckdzkj.com/images/imgs/inspection-expire.png' },
|
||
{ label: '待处理', value: 0, icon: 'https://wuye.ckdzkj.com/images/imgs/inspection-pending.png' }
|
||
])
|
||
|
||
const switchTab = (tab) => {
|
||
activeTab.value = tab
|
||
feeLoading.value = true
|
||
fetchFeeSummary()
|
||
}
|
||
|
||
const onProjectChange = (e) => {
|
||
const opt = projectOptions.value[e.detail.value]
|
||
selectedProject.value = opt.value
|
||
selectedProjectLabel.value = opt.label
|
||
feeLoading.value = true
|
||
fetchManagedArea(opt.value)
|
||
fetchRate(opt.value)
|
||
fetchFeeSummary()
|
||
fetchParkingFee(opt.value)
|
||
fetchDepositBalance(opt.value)
|
||
fetchInspection(opt.value)
|
||
}
|
||
|
||
const onFeeTypeChange = (opt) => {
|
||
selectedFeeType.value = opt.value
|
||
selectedFeeTypeLabel.value = opt.label
|
||
feeLoading.value = true
|
||
fetchFeeSummary()
|
||
}
|
||
|
||
const fetchVillageList = () => {
|
||
return getVillageList().then(res => {
|
||
if (res.code === 200 && res.data) {
|
||
projectOptions.value = [{ value: '', label: '全部项目' }, ...res.data.map(item => ({ value: String(item.id), label: item.villageName }))]
|
||
selectedProject.value = ''
|
||
selectedProjectLabel.value = '全部项目'
|
||
}
|
||
}).catch(() => {})
|
||
}
|
||
|
||
const fetchManagedArea = (villageId) => {
|
||
return getManagedArea({ villageId: villageId || '' }).then(res => {
|
||
if (res.code === 200 && res.data) {
|
||
manageArea.value = res.data.manageArea
|
||
serviceHouseholds.value = res.data.serviceHouseCount
|
||
}
|
||
}).catch(() => {})
|
||
}
|
||
|
||
const fetchRate = (villageId) => {
|
||
return getRate({ villageId: villageId || '' }).then(res => {
|
||
if (res.code === 200 && res.data) {
|
||
occupancyRate.value = res.data.occupancyRate
|
||
rentalRate.value = res.data.rentalRate
|
||
}
|
||
}).catch(() => {})
|
||
}
|
||
// 车位收费总览
|
||
const fetchParkingFee = (villageId) => {
|
||
return getParkingFee({ villageId: villageId || '' }).then(res => {
|
||
if (res.code === 200 && res.data) {
|
||
const d = res.data
|
||
parkingCount.value = Number(d.parkingCount) || 0
|
||
parkingYearReceivable.value = Number(d.receivableAmount) || 0
|
||
parkingYearReceived.value = Number(d.receivedAmount) || 0
|
||
parkingArcbarPercent.value = d.receivedPercent
|
||
}
|
||
}).catch(() => {})
|
||
}
|
||
|
||
// 收费总览
|
||
const fetchFeeSummary = () => {
|
||
return getFeeSummary({
|
||
villageId: selectedProject.value || '',
|
||
chargeTypeId: selectedFeeType.value || '',
|
||
timeScope: activeTab.value
|
||
}).then(res => {
|
||
if (res.code === 200 && res.data) {
|
||
const d = res.data
|
||
arcbarPercent.value = Number(d.receivedPercent) || 0
|
||
metricList.value = [
|
||
{ label: '应收总额', value: (Number(d.receivableAmount) || 0).toFixed(2), yoy: formatRate(d.receivableYoyRate), mom: formatRate(d.receivableMomRate) },
|
||
{ label: '实收总额', value: (Number(d.receivedAmount) || 0).toFixed(2), yoy: formatRate(d.receivedYoyRate), mom: formatRate(d.receivedMomRate) },
|
||
{ label: '欠收总额', value: (Number(d.unpaidAmount) || 0).toFixed(2), yoy: formatRate(d.unpaidYoyRate), mom: formatRate(d.unpaidMomRate) }
|
||
]
|
||
}
|
||
}).catch(() => {}).finally(() => {
|
||
feeLoading.value = false
|
||
})
|
||
}
|
||
|
||
const formatRate = (val) => {
|
||
if (val == null || val === '') return '-'
|
||
const num = Number(val)
|
||
if (isNaN(num)) return '-'
|
||
return (num >= 0 ? '+' : '') + num.toFixed(2) + '%'
|
||
}
|
||
|
||
const fetchFeeTypes = () => {
|
||
return getFeeTypes().then(res => {
|
||
if (res.code === 200 && res.data) {
|
||
feeTypeOptions.value = [{ value: '', label: '全部收费类型' }, ...res.data.map(item => ({ value: String(item.id), label: item.name }))]
|
||
selectedFeeType.value = ''
|
||
selectedFeeTypeLabel.value = '全部收费类型'
|
||
}
|
||
}).catch(() => {})
|
||
}
|
||
// 押金余额统计
|
||
const fetchDepositBalance = (villageId) => {
|
||
return getDepositBalance({ villageId: villageId}).then(res => {
|
||
if (res.code === 200 && res.data) {
|
||
const d = res.data
|
||
depositBalance.value = d.totalBalance
|
||
depositItems.value = d.items || []
|
||
}
|
||
}).catch(() => {})
|
||
}
|
||
// 巡检总览
|
||
const fetchInspection = (villageId) => {
|
||
return getInspection({ villageId: villageId || '' }).then(res => {
|
||
if (res.code === 200 && res.data) {
|
||
const d = res.data
|
||
inspectionList.value = [
|
||
{ label: '巡检任务', value: d.taskCount || 0, icon: 'https://wuye.ckdzkj.com/images/imgs/inspection-task.png' },
|
||
{ label: '已完成', value: d.completedCount || 0, icon: 'https://wuye.ckdzkj.com/images/imgs/inspection-done.png' },
|
||
{ label: '已过期', value: d.expiredCount || 0, icon: 'https://wuye.ckdzkj.com/images/imgs/inspection-expire.png' },
|
||
{ label: '待处理', value: d.pendingCount || 0, icon: 'https://wuye.ckdzkj.com/images/imgs/inspection-pending.png' }
|
||
]
|
||
}
|
||
}).catch(() => {})
|
||
}
|
||
|
||
// 加载全部数据(首屏 / 下拉刷新共用)
|
||
const loadAll = () => {
|
||
return Promise.all([fetchVillageList(), fetchFeeTypes()]).then(() => {
|
||
return Promise.allSettled([
|
||
fetchManagedArea(selectedProject.value),
|
||
fetchRate(selectedProject.value),
|
||
fetchFeeSummary(),
|
||
fetchParkingFee(selectedProject.value),
|
||
fetchDepositBalance(selectedProject.value),
|
||
fetchInspection(selectedProject.value)
|
||
])
|
||
})
|
||
}
|
||
|
||
onShow(() => {
|
||
loadAll()
|
||
})
|
||
|
||
onPullDownRefresh(async () => {
|
||
try {
|
||
await loadAll()
|
||
} finally {
|
||
uni.stopPullDownRefresh()
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.page_padding {
|
||
background: #F9F9F9;
|
||
padding: 0 30rpx;
|
||
max-height: none !important;
|
||
overflow-y: auto !important;
|
||
}
|
||
|
||
.filter-section {
|
||
padding: 16rpx 0 12rpx;
|
||
.filter-row-full .filter-label {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
background: #fff;
|
||
border-radius: 12rpx;
|
||
padding: 25rpx 24rpx;
|
||
font-size: 26rpx;
|
||
color: #333;
|
||
.filter-arrow { font-size: 20rpx; color: #999; margin-left: 8rpx; }
|
||
}
|
||
}
|
||
|
||
.overview-card {
|
||
margin-top: 20rpx;
|
||
background: linear-gradient(to right, #4D5DFA, #7A86FF);
|
||
border-radius: 8rpx;
|
||
padding: 30rpx 0;
|
||
display: flex;
|
||
align-items: center;
|
||
.card-left, .card-right {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
}
|
||
.card-num {
|
||
font-size: 44rpx;
|
||
font-weight: 700;
|
||
color: #fff;
|
||
line-height: 1.2;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
.card-label {
|
||
font-size: 24rpx;
|
||
color: rgba(255,255,255,.85);
|
||
}
|
||
}
|
||
|
||
.rate-row {
|
||
margin-top: 20rpx;
|
||
display: flex;
|
||
.rate-item {
|
||
flex: 1;
|
||
border-radius: 8rpx;
|
||
padding: 30rpx 18px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
background-size: cover;
|
||
background-position: center;
|
||
}
|
||
.rate-item + .rate-item {
|
||
margin-left: 30rpx;
|
||
}
|
||
.rate-occupy {
|
||
background-image: url('https://wuye.ckdzkj.com/images/imgs/rate-occupy.png');
|
||
}
|
||
.rate-rental {
|
||
background-image: url('https://wuye.ckdzkj.com/images/imgs/rate-rental.png');
|
||
}
|
||
.rate-text { font-size: 24rpx; color: #fff; margin-bottom: 8rpx; }
|
||
.rate-num { font-size: 44rpx; font-weight: 700; color: #fff; line-height: 1.2; }
|
||
}
|
||
</style>
|