Merge branch 'master' of http://gitea.bugtc.com/zhouyanli/property-manager-side
This commit is contained in:
@@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<view class="setting-container">
|
||||
<view class="status_bar"></view>
|
||||
<!-- <view class="status_bar"></view> -->
|
||||
<!-- 顶部导航栏 -->
|
||||
<view class="nav-bar">
|
||||
<!-- <view class="nav-bar">
|
||||
<view class="nav-left" @tap="goBack">
|
||||
<uni-icons type="left" size="22" color="#000"></uni-icons>
|
||||
</view>
|
||||
<view class="nav-title">设置</view>
|
||||
<view class="nav-right"></view>
|
||||
</view>
|
||||
</view> -->
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<scroll-view class="content" scroll-y="true">
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<view class="deposit-card">
|
||||
<view class="deposit-card-header">
|
||||
<text class="deposit-title">押金余额统计</text>
|
||||
<text class="deposit-balance">余额合计{{ depositBalance }}元</text>
|
||||
</view>
|
||||
<view class="deposit-chart">
|
||||
<qiun-data-charts type="column" :chartData="depositColumnData" :opts="depositColumnOpts" />
|
||||
<view class="deposit-empty-mask" v-if="depositItems.length === 0">
|
||||
<text class="empty-text">暂无数据</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
depositBalance: { type: String, default: '0' },
|
||||
depositItems: { type: Array, default: () => [] }
|
||||
})
|
||||
|
||||
const depositColumnData = computed(() => {
|
||||
const categories = props.depositItems.map(item => item.typeName)
|
||||
const data = props.depositItems.map(item => (item.balance / 10000))
|
||||
return {
|
||||
categories,
|
||||
series: [{ name: '余额', data }]
|
||||
}
|
||||
})
|
||||
|
||||
const depositColumnOpts = computed(() => ({
|
||||
color: ['#4D5DFA'],
|
||||
padding: [20, 10, 0, 10],
|
||||
enableScroll: false,
|
||||
legend: { show: false },
|
||||
xAxis: {
|
||||
disableGrid: true,
|
||||
axisLineColor: '#EAEAEA',
|
||||
dashLength: 4,
|
||||
fontColor: '#666',
|
||||
fontSize: 12
|
||||
},
|
||||
yAxis: {
|
||||
showTitle: true,
|
||||
data: [{
|
||||
title: '金额(万)',
|
||||
min: 0,
|
||||
axisLineColor: '#EAEAEA',
|
||||
fontColor: '#666',
|
||||
fontSize: 12,
|
||||
titleFontSize: 12,
|
||||
titleFontColor: '#666'
|
||||
}],
|
||||
gridType: 'dash',
|
||||
dashLength: 4,
|
||||
gridColor: '#F2F2F2',
|
||||
},
|
||||
extra: {
|
||||
column: {
|
||||
type: 'group',
|
||||
width: 30,
|
||||
activeBgColor: '#4D5DFA',
|
||||
activeBgOpacity: 0.08,
|
||||
dataLabel: {
|
||||
show: true,
|
||||
position: 'top',
|
||||
fontSize: 12,
|
||||
color: '#333',
|
||||
formatter: (val) => val.toFixed(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.deposit-card {
|
||||
margin-top: 20rpx;
|
||||
background: #fff;
|
||||
border-radius: 8rpx;
|
||||
padding: 30rpx 24rpx;
|
||||
|
||||
.deposit-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20rpx;
|
||||
.deposit-title { font-size: 32rpx; font-weight: 600; color: #222; }
|
||||
.deposit-balance { font-size: 28rpx; color: #333; }
|
||||
}
|
||||
|
||||
.deposit-chart {
|
||||
width: 100%;
|
||||
height: 420rpx;
|
||||
position: relative;
|
||||
.deposit-empty-mask {
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #fff;
|
||||
.empty-text { font-size: 26rpx; color: #999; }
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,169 @@
|
||||
<template>
|
||||
<view class="fee-card">
|
||||
<view class="fee-card-header">
|
||||
<text class="fee-title">收费总览</text>
|
||||
<picker mode="selector" :range="feeTypeOptions" range-key="label" @change="onFeeTypeChange">
|
||||
<view class="fee-type-picker">
|
||||
<text>{{ selectedFeeTypeLabel || '全部收费类型' }}</text>
|
||||
<text class="filter-arrow">▼</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="fee-tabs">
|
||||
<view class="fee-tab-item" :class="{ active: activeTab === 'day' }" @click="switchTab('day')">本日</view>
|
||||
<view class="fee-tab-item" :class="{ active: activeTab === 'month' }" @click="switchTab('month')">本月</view>
|
||||
<view class="fee-tab-item" :class="{ active: activeTab === 'year' }" @click="switchTab('year')">本年</view>
|
||||
</view>
|
||||
|
||||
<view class="fee-content">
|
||||
<view class="fee-ring">
|
||||
<qiun-data-charts type="arcbar" :chartData="arcbarData" :opts="arcbarOpts" />
|
||||
</view>
|
||||
|
||||
<view class="fee-metrics">
|
||||
<view class="metric-row" v-for="item in metricList" :key="item.label">
|
||||
<view class="metric-info">
|
||||
<text class="metric-label">{{ item.label }}(元)</text>
|
||||
<text class="metric-num">{{ item.value }}</text>
|
||||
</view>
|
||||
<view class="metric-compare">
|
||||
<text class="compare-line">同比 <text class="compare-num" :class="compareClass(item.yoy)">{{ item.yoy }}</text></text>
|
||||
<text class="compare-line">环比 <text class="compare-num" :class="compareClass(item.mom)">{{ item.mom }}</text></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
feeTypeOptions: { type: Array, default: () => [] },
|
||||
selectedFeeTypeLabel: { type: String, default: '全部收费类型' },
|
||||
activeTab: { type: String, default: 'day' },
|
||||
arcbarPercent: { type: Number, default: 0 },
|
||||
metricList: { type: Array, default: () => [] }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['tab-change', 'fee-type-change'])
|
||||
|
||||
const arcbarData = computed(() => ({
|
||||
series: [{ name: '实收占比', data: props.arcbarPercent }]
|
||||
}))
|
||||
|
||||
const arcbarOpts = computed(() => ({
|
||||
color: ["#4D5DFA"],
|
||||
title: { name: Math.round(props.arcbarPercent * 100) + '%', fontSize: 16, color: '#4D5DFA' },
|
||||
subtitle: { name: '' },
|
||||
extra: {
|
||||
arcbar: {
|
||||
type: 'circle',
|
||||
width: 16,
|
||||
backgroundColor: '#EAEAEA',
|
||||
startAngle: 1.5,
|
||||
endAngle: 0.25,
|
||||
gap: 2,
|
||||
lineCap: 'butt'
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
const compareClass = (val) => {
|
||||
if (val && val.startsWith('-')) return 'down'
|
||||
if (val && val.startsWith('+')) return 'up'
|
||||
return ''
|
||||
}
|
||||
|
||||
const switchTab = (tab) => { emit('tab-change', tab) }
|
||||
|
||||
const onFeeTypeChange = (e) => {
|
||||
const opt = props.feeTypeOptions[e.detail.value]
|
||||
emit('fee-type-change', opt)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.fee-card {
|
||||
margin-top: 20rpx;
|
||||
background: #fff;
|
||||
border-radius: 8rpx;
|
||||
padding: 30rpx 24rpx;
|
||||
|
||||
.fee-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24rpx;
|
||||
.fee-title { font-size: 32rpx; font-weight: 600; color: #222; }
|
||||
.fee-type-picker { display: flex; align-items: center; font-size: 24rpx; color: #666; border: 2rpx solid #DCDFE6; border-radius: 6rpx; padding: 8rpx 16rpx; }
|
||||
.filter-arrow { font-size: 18rpx; color: #999; margin-left: 6rpx; }
|
||||
}
|
||||
|
||||
.fee-tabs {
|
||||
display: flex;
|
||||
.fee-tab-item {
|
||||
text-align: center;
|
||||
padding: 14rpx 32rpx;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
position: relative;
|
||||
&.active {
|
||||
color: #4D5DFA;
|
||||
font-weight: 600;
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 60rpx;
|
||||
height: 4rpx;
|
||||
background: #4D5DFA;
|
||||
border-radius: 2rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fee-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 30rpx;
|
||||
|
||||
.fee-ring {
|
||||
width: 240rpx;
|
||||
height: 180rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.fee-metrics {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
margin-left: 20rpx;
|
||||
|
||||
.metric-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.metric-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.metric-label { font-size: 28rpx; color: #666; }
|
||||
.metric-num { font-size: 32rpx; color: #333; font-weight: 600; margin-top: 2rpx; }
|
||||
}
|
||||
.metric-compare {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
.compare-line { font-size: 24rpx; color: #666; line-height: 1.7; }
|
||||
.compare-num { font-weight: 600; &.up { color: #67C23A; } &.down { color: #F56C6C; } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<view class="inspection-section">
|
||||
<view class="inspection-title">巡检总览</view>
|
||||
<view class="inspection-grid">
|
||||
<view class="inspection-item" v-for="item in inspectionList" :key="item.label">
|
||||
<view class="inspection-item-left">
|
||||
<text class="inspection-item-label">{{ item.label }}</text>
|
||||
<text class="inspection-item-num">{{ item.value }}个</text>
|
||||
</view>
|
||||
<view class="inspection-item-right">
|
||||
<image class="inspection-icon" :src="item.icon" mode="aspectFit" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
inspectionList: { type: Array, default: () => [] }
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.inspection-section {
|
||||
margin: 20rpx 0;
|
||||
|
||||
.inspection-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.inspection-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20rpx;
|
||||
|
||||
.inspection-item {
|
||||
width: calc(50% - 10rpx);
|
||||
background: #fff;
|
||||
border-radius: 8rpx;
|
||||
padding: 30rpx 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
|
||||
.inspection-item-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.inspection-item-label { font-size: 28rpx; color: #666; margin-bottom: 6rpx; }
|
||||
.inspection-item-num { font-size: 32rpx; color: #333; font-weight: 600; }
|
||||
}
|
||||
|
||||
.inspection-item-right {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
flex-shrink: 0;
|
||||
.inspection-icon {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<view class="parking-card">
|
||||
<view class="parking-card-header">
|
||||
<text class="parking-title">车位收费总览</text>
|
||||
</view>
|
||||
<view class="parking-count-row">
|
||||
<text class="parking-count-label">车位数量(个)</text>
|
||||
<text class="parking-count-num">{{ parkingCount }}</text>
|
||||
</view>
|
||||
<view class="parking-divider"></view>
|
||||
<view class="parking-fee-content">
|
||||
<view class="parking-fee-left">
|
||||
<view class="parking-fee-item">
|
||||
<text class="parking-fee-label">本年应收(元)</text>
|
||||
<text class="parking-fee-value">{{ parkingYearReceivable }}</text>
|
||||
</view>
|
||||
<view class="parking-fee-item">
|
||||
<text class="parking-fee-label">本年已收(元)</text>
|
||||
<text class="parking-fee-value">{{ parkingYearReceived }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="parking-fee-right">
|
||||
<qiun-data-charts type="arcbar" :chartData="parkingArcbarData" :opts="parkingArcbarOpts" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
parkingCount: { type: Number, default: 0 },
|
||||
parkingYearReceivable: { type: Number, default: 0 },
|
||||
parkingYearReceived: { type: Number, default: 0 },
|
||||
parkingArcbarPercent: { type: Number, default: 0 }
|
||||
})
|
||||
|
||||
const parkingArcbarData = computed(() => ({
|
||||
series: [{ name: '实收占比', data: props.parkingArcbarPercent }]
|
||||
}))
|
||||
|
||||
const parkingArcbarOpts = computed(() => ({
|
||||
color: ["#4D5DFA"],
|
||||
title: { name: Math.round(props.parkingArcbarPercent * 100) + '%', fontSize: 16, color: '#4D5DFA' },
|
||||
subtitle: { name: '' },
|
||||
extra: {
|
||||
arcbar: {
|
||||
type: 'circle',
|
||||
width: 16,
|
||||
backgroundColor: '#EAEAEA',
|
||||
startAngle: 1.5,
|
||||
endAngle: 0.25,
|
||||
gap: 2,
|
||||
lineCap: 'butt'
|
||||
}
|
||||
}
|
||||
}))
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.parking-card {
|
||||
margin-top: 20rpx;
|
||||
background: #fff;
|
||||
border-radius: 8rpx;
|
||||
padding: 30rpx 24rpx;
|
||||
|
||||
.parking-card-header {
|
||||
margin-bottom: 20rpx;
|
||||
.parking-title { font-size: 32rpx; font-weight: 600; color: #222; }
|
||||
}
|
||||
|
||||
.parking-count-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 20rpx;
|
||||
.parking-count-label { font-size: 28rpx; color: #666; margin-bottom: 6rpx; }
|
||||
.parking-count-num { font-size: 32rpx; color: #333; font-weight: 600; }
|
||||
}
|
||||
|
||||
.parking-divider {
|
||||
height: 1rpx;
|
||||
background: #EAEAEA;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.parking-fee-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.parking-fee-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
.parking-fee-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
.parking-fee-label { font-size: 28rpx; color: #666; margin-bottom: 4rpx; }
|
||||
.parking-fee-value { font-size: 32rpx; color: #333; font-weight: 600; }
|
||||
}
|
||||
}
|
||||
|
||||
.parking-fee-right {
|
||||
width: 240rpx;
|
||||
height: 180rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
329
wuye-manager-uniapp/pageSubPack/statistics/statistics.vue
Normal file
329
wuye-manager-uniapp/pageSubPack/statistics/statistics.vue
Normal file
@@ -0,0 +1,329 @@
|
||||
<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"
|
||||
@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 } 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 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
|
||||
fetchFeeSummary()
|
||||
}
|
||||
|
||||
const onProjectChange = (e) => {
|
||||
const opt = projectOptions.value[e.detail.value]
|
||||
selectedProject.value = opt.value
|
||||
selectedProjectLabel.value = opt.label
|
||||
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
|
||||
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) => {
|
||||
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) => {
|
||||
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) => {
|
||||
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 != null ? Number(d.receivedPercent) / 100 : 0
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 收费总览
|
||||
const fetchFeeSummary = () => {
|
||||
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(() => {})
|
||||
}
|
||||
|
||||
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) => {
|
||||
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) => {
|
||||
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(() => {})
|
||||
}
|
||||
|
||||
onShow(() => {
|
||||
Promise.all([fetchVillageList(), fetchFeeTypes()]).then(() => {
|
||||
fetchManagedArea(selectedProject.value)
|
||||
fetchRate(selectedProject.value)
|
||||
fetchFeeSummary()
|
||||
fetchParkingFee(selectedProject.value)
|
||||
fetchDepositBalance(selectedProject.value)
|
||||
fetchInspection(selectedProject.value)
|
||||
})
|
||||
})
|
||||
</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;
|
||||
gap: 30rpx;
|
||||
.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-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>
|
||||
Reference in New Issue
Block a user