收费总览页面

This commit is contained in:
zhouyanli
2026-08-07 16:36:05 +08:00
parent d8faa12083
commit 5b23e48e2c
12 changed files with 163 additions and 61 deletions

View File

@@ -4,6 +4,7 @@ import { onLaunch, onShow, onHide } from '@dcloudio/uni-app'
let pushCIDRetryCount = 0 let pushCIDRetryCount = 0
const MAX_RETRY = 5 const MAX_RETRY = 5
let retryTimer = null let retryTimer = null
let pushCIDFetching = false // 防止并发请求
// 应用启动时执行 // 应用启动时执行
onLaunch(() => { onLaunch(() => {
@@ -13,27 +14,32 @@ onLaunch(() => {
}) })
// #ifdef APP-PLUS // #ifdef APP-PLUS
// 只有 App 平台才获取 CID,延迟 1 秒等推送服务就绪 // 延迟 2 秒等推送服务完全就绪后再获取 CID
setTimeout(() => { setTimeout(() => {
getPushCID() getPushCID()
}, 1000) }, 2000)
// #endif // #endif
}) })
onShow(() => { onShow(() => {
console.log('App Show') console.log('App Show')
// #ifdef APP-PLUS // #ifdef APP-PLUS
// 每次回到前台都重新获取 CID,防止 CID 过期失效 // 从后台恢复时,如果 CID 已经获取过且未过期,不再重复请求
const existingCid = uni.getStorageSync('push_cid')
if (existingCid) {
console.log('已有缓存 CID,跳过重复获取')
return
}
pushCIDRetryCount = 0 pushCIDRetryCount = 0
setTimeout(() => { setTimeout(() => {
refreshPushCID() refreshPushCID()
}, 500) }, 1000)
// #endif // #endif
}) })
onHide(() => { onHide(() => {
console.log('App Hide') console.log('App Hide')
// 清除重试定时器,避免后台无效重试 pushCIDFetching = false
if (retryTimer) { if (retryTimer) {
clearTimeout(retryTimer) clearTimeout(retryTimer)
retryTimer = null retryTimer = null
@@ -43,9 +49,12 @@ onHide(() => {
// 刷新 CID(每次 onShow 调用,比对更新) // 刷新 CID(每次 onShow 调用,比对更新)
function refreshPushCID() { function refreshPushCID() {
if (pushCIDFetching) return
const oldCid = uni.getStorageSync('push_cid') || '' const oldCid = uni.getStorageSync('push_cid') || ''
pushCIDFetching = true
uni.getPushClientId({ uni.getPushClientId({
success: res => { success: res => {
pushCIDFetching = false
const newCid = res.cid const newCid = res.cid
if (!newCid) { if (!newCid) {
console.warn('获取到的 CID 为空,视为失败') console.warn('获取到的 CID 为空,视为失败')
@@ -62,6 +71,7 @@ function refreshPushCID() {
} }
}, },
fail: err => { fail: err => {
pushCIDFetching = false
console.error('获取推送 CID 失败', err) console.error('获取推送 CID 失败', err)
scheduleRetry() scheduleRetry()
} }
@@ -70,8 +80,11 @@ function refreshPushCID() {
// 启动时首次获取 CID(重试更积极) // 启动时首次获取 CID(重试更积极)
function getPushCID() { function getPushCID() {
if (pushCIDFetching) return
pushCIDFetching = true
uni.getPushClientId({ uni.getPushClientId({
success: res => { success: res => {
pushCIDFetching = false
const cid = res.cid const cid = res.cid
if (!cid) { if (!cid) {
console.warn('获取到的 CID 为空,视为失败') console.warn('获取到的 CID 为空,视为失败')
@@ -84,6 +97,7 @@ function getPushCID() {
// TODO: 将 CID 上报到服务器 // TODO: 将 CID 上报到服务器
}, },
fail: err => { fail: err => {
pushCIDFetching = false
console.error('获取推送 CID 失败', err) console.error('获取推送 CID 失败', err)
scheduleRetry() scheduleRetry()
} }

View File

@@ -11,6 +11,7 @@ export default function request(url, data = {}, method = 'GET') {
url: baseUrl + url, url: baseUrl + url,
data: data, data: data,
method: method, method: method,
timeout: 15000,
header: { header: {
Authorization: 'Bearer ' + (uni.getStorageSync('token') || ''), Authorization: 'Bearer ' + (uni.getStorageSync('token') || ''),
clientid: 'butler', clientid: 'butler',

View File

@@ -24,3 +24,11 @@ export const getFeeSummary = data => {
export const getParkingFee = data => { export const getParkingFee = data => {
return get('/api/butler/feeOverview/parking', data) return get('/api/butler/feeOverview/parking', data)
} }
// 押金余额统计
export const getDepositBalance = data => {
return get('/api/butler/feeOverview/depositBalance', data)
}
// 巡检总览
export const getInspection = data => {
return get('/api/butler/feeOverview/inspection', data)
}

View File

@@ -10,7 +10,7 @@
"nvueStyleCompiler" : "uni-app", "nvueStyleCompiler" : "uni-app",
"compilerVersion" : 3, "compilerVersion" : 3,
"splashscreen" : { "splashscreen" : {
"alwaysShowBeforeRender" : true, "alwaysShowBeforeRender" : false,
"waiting" : true, "waiting" : true,
"autoclose" : true, "autoclose" : true,
"delay" : 0, "delay" : 0,

View File

@@ -1,14 +1,14 @@
<template> <template>
<view class="setting-container"> <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"> <view class="nav-left" @tap="goBack">
<uni-icons type="left" size="22" color="#000"></uni-icons> <uni-icons type="left" size="22" color="#000"></uni-icons>
</view> </view>
<view class="nav-title">设置</view> <view class="nav-title">设置</view>
<view class="nav-right"></view> <view class="nav-right"></view>
</view> </view> -->
<!-- 内容区域 --> <!-- 内容区域 -->
<scroll-view class="content" scroll-y="true"> <scroll-view class="content" scroll-y="true">

View File

@@ -6,6 +6,9 @@
</view> </view>
<view class="deposit-chart"> <view class="deposit-chart">
<qiun-data-charts type="column" :chartData="depositColumnData" :opts="depositColumnOpts" /> <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>
</view> </view>
</template> </template>
@@ -15,16 +18,17 @@ import { computed } from 'vue'
const props = defineProps({ const props = defineProps({
depositBalance: { type: String, default: '0' }, depositBalance: { type: String, default: '0' },
depositRenovation: { type: Number, default: 0 }, depositItems: { type: Array, default: () => [] }
depositRental: { type: Number, default: 0 }
}) })
const depositColumnData = computed(() => ({ const depositColumnData = computed(() => {
categories: ['装修押金', '租赁押金'], const categories = props.depositItems.map(item => item.typeName)
series: [ const data = props.depositItems.map(item => (item.balance / 10000))
{ name: '余额', data: [props.depositRenovation, props.depositRental] } return {
] categories,
})) series: [{ name: '余额', data }]
}
})
const depositColumnOpts = computed(() => ({ const depositColumnOpts = computed(() => ({
color: ['#4D5DFA'], color: ['#4D5DFA'],
@@ -34,22 +38,24 @@ const depositColumnOpts = computed(() => ({
xAxis: { xAxis: {
disableGrid: true, disableGrid: true,
axisLineColor: '#EAEAEA', axisLineColor: '#EAEAEA',
dashLength: 4,
fontColor: '#666', fontColor: '#666',
fontSize: 12 fontSize: 12
}, },
yAxis: { yAxis: {
showTitle: true,
data: [{ data: [{
title: '万元', title: '金额(万)',
min: 0, min: 0,
max: 80,
axisLineColor: '#EAEAEA', axisLineColor: '#EAEAEA',
fontColor: '#666', fontColor: '#666',
fontSize: 12, fontSize: 12,
titleFontSize: 12, titleFontSize: 12,
titleFontColor: '#666' titleFontColor: '#666'
}], }],
splitLine: { show: true, lineStyle: { color: '#F2F2F2' } }, gridType: 'dash',
calibration: { step: 0.5 } dashLength: 4,
gridColor: '#F2F2F2',
}, },
extra: { extra: {
column: { column: {
@@ -88,6 +94,16 @@ const depositColumnOpts = computed(() => ({
.deposit-chart { .deposit-chart {
width: 100%; width: 100%;
height: 420rpx; 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> </style>

View File

@@ -58,8 +58,7 @@
<!-- 押金余额统计 --> <!-- 押金余额统计 -->
<DepositCard <DepositCard
:deposit-balance="depositBalance" :deposit-balance="depositBalance"
:deposit-renovation="depositRenovation" :deposit-items="depositItems"
:deposit-rental="depositRental"
/> />
<!-- 巡检总览 --> <!-- 巡检总览 -->
@@ -77,7 +76,7 @@ import FeeOverviewCard from './components/FeeOverviewCard.vue'
import ParkingFeeCard from './components/ParkingFeeCard.vue' import ParkingFeeCard from './components/ParkingFeeCard.vue'
import DepositCard from './components/DepositCard.vue' import DepositCard from './components/DepositCard.vue'
import InspectionOverview from './components/InspectionOverview.vue' import InspectionOverview from './components/InspectionOverview.vue'
import { getManagedArea, getRate, getFeeSummary, getParkingFee } from '@/api/statisticsApi.js' import { getManagedArea, getRate, getFeeSummary, getParkingFee, getDepositBalance, getInspection } from '@/api/statisticsApi.js'
@@ -109,16 +108,15 @@ const parkingYearReceived = ref(0)
const parkingArcbarPercent = ref(0) const parkingArcbarPercent = ref(0)
// 押金余额统计 // 押金余额统计
const depositBalance = ref('33322') const depositBalance = ref('0')
const depositRenovation = ref(18) const depositItems = ref([])
const depositRental = ref(23)
// 巡检总览 // 巡检总览
const inspectionList = ref([ const inspectionList = ref([
{ label: '巡检任务', value: 124, icon: 'https://wuye.ckdzkj.com/images/imgs/inspection-task.png' }, { label: '巡检任务', value: 0, icon: 'https://wuye.ckdzkj.com/images/imgs/inspection-task.png' },
{ label: '已完成', value: 98, icon: 'https://wuye.ckdzkj.com/images/imgs/inspection-done.png' }, { label: '已完成', value: 0, icon: 'https://wuye.ckdzkj.com/images/imgs/inspection-done.png' },
{ label: '已过期', value: 12, icon: 'https://wuye.ckdzkj.com/images/imgs/inspection-expire.png' }, { label: '已过期', value: 0, icon: 'https://wuye.ckdzkj.com/images/imgs/inspection-expire.png' },
{ label: '待处理', value: 14, icon: 'https://wuye.ckdzkj.com/images/imgs/inspection-pending.png' } { label: '待处理', value: 0, icon: 'https://wuye.ckdzkj.com/images/imgs/inspection-pending.png' }
]) ])
const switchTab = (tab) => { const switchTab = (tab) => {
@@ -134,6 +132,8 @@ const onProjectChange = (e) => {
fetchRate(opt.value) fetchRate(opt.value)
fetchFeeSummary() fetchFeeSummary()
fetchParkingFee(opt.value) fetchParkingFee(opt.value)
fetchDepositBalance(opt.value)
fetchInspection(opt.value)
} }
const onFeeTypeChange = (opt) => { const onFeeTypeChange = (opt) => {
@@ -169,19 +169,20 @@ const fetchRate = (villageId) => {
} }
}).catch(() => {}) }).catch(() => {})
} }
// 车位收费总览
const fetchParkingFee = (villageId) => { const fetchParkingFee = (villageId) => {
getParkingFee({ villageId: villageId || '' }).then(res => { getParkingFee({ villageId: villageId || '' }).then(res => {
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
const d = res.data const d = res.data
parkingCount.value = d.parkingCount || 0 parkingCount.value = Number(d.parkingCount) || 0
parkingYearReceivable.value = d.receivableAmount || 0 parkingYearReceivable.value = Number(d.receivableAmount) || 0
parkingYearReceived.value = d.receivedAmount || 0 parkingYearReceived.value = Number(d.receivedAmount) || 0
parkingArcbarPercent.value = d.receivedPercent != null ? d.receivedPercent / 100 : 0 parkingArcbarPercent.value = d.receivedPercent != null ? Number(d.receivedPercent) / 100 : 0
} }
}).catch(() => {}) }).catch(() => {})
} }
// 收费总览
const fetchFeeSummary = () => { const fetchFeeSummary = () => {
getFeeSummary({ getFeeSummary({
villageId: selectedProject.value || '', villageId: selectedProject.value || '',
@@ -190,19 +191,21 @@ const fetchFeeSummary = () => {
}).then(res => { }).then(res => {
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
const d = res.data const d = res.data
arcbarPercent.value = d.receivedPercent != null ? d.receivedPercent / 100 : 0 arcbarPercent.value = Number(d.receivedPercent) || 0
metricList.value = [ metricList.value = [
{ label: '应收总额', value: d.receivableAmount != null ? d.receivableAmount.toFixed(2) : '0.00', yoy: formatRate(d.receivableYoyRate), mom: formatRate(d.receivableMomRate) }, { label: '应收总额', value: (Number(d.receivableAmount) || 0).toFixed(2), yoy: formatRate(d.receivableYoyRate), mom: formatRate(d.receivableMomRate) },
{ label: '实收总额', value: d.receivedAmount != null ? d.receivedAmount.toFixed(2) : '0.00', yoy: formatRate(d.receivedYoyRate), mom: formatRate(d.receivedMomRate) }, { label: '实收总额', value: (Number(d.receivedAmount) || 0).toFixed(2), yoy: formatRate(d.receivedYoyRate), mom: formatRate(d.receivedMomRate) },
{ label: '欠收总额', value: d.unpaidAmount != null ? d.unpaidAmount.toFixed(2) : '0.00', yoy: formatRate(d.unpaidYoyRate), mom: formatRate(d.unpaidMomRate) } { label: '欠收总额', value: (Number(d.unpaidAmount) || 0).toFixed(2), yoy: formatRate(d.unpaidYoyRate), mom: formatRate(d.unpaidMomRate) }
] ]
} }
}).catch(() => {}) }).catch(() => {})
} }
const formatRate = (val) => { const formatRate = (val) => {
if (val == null) return '-' if (val == null || val === '') return '-'
return (val >= 0 ? '+' : '') + val.toFixed(2) + '%' const num = Number(val)
if (isNaN(num)) return '-'
return (num >= 0 ? '+' : '') + num.toFixed(2) + '%'
} }
const fetchFeeTypes = () => { const fetchFeeTypes = () => {
@@ -214,6 +217,30 @@ const fetchFeeTypes = () => {
} }
}).catch(() => {}) }).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(() => { onShow(() => {
Promise.all([fetchVillageList(), fetchFeeTypes()]).then(() => { Promise.all([fetchVillageList(), fetchFeeTypes()]).then(() => {
@@ -221,6 +248,8 @@ onShow(() => {
fetchRate(selectedProject.value) fetchRate(selectedProject.value)
fetchFeeSummary() fetchFeeSummary()
fetchParkingFee(selectedProject.value) fetchParkingFee(selectedProject.value)
fetchDepositBalance(selectedProject.value)
fetchInspection(selectedProject.value)
}) })
}) })
</script> </script>

View File

@@ -262,8 +262,7 @@
"pages": [{ "pages": [{
"path": "setting", "path": "setting",
"style": { "style": {
"navigationBarTitleText": "设置", "navigationBarTitleText": "设置"
"navigationStyle": "custom"
} }
}, },
{ {

View File

@@ -136,6 +136,13 @@ import { usePermStore } from '@/stores/usePermStore'
const permStore = usePermStore() const permStore = usePermStore()
// 首次加载标记:onLoad 和 onShow 在首次进入时都会触发,用此标记避免重复请求
const hasInit = ref(false)
// banner 缓存 key 和缓存时长(30分钟)
const BANNER_CACHE_KEY = 'home_banner_cache'
const BANNER_CACHE_TIME = 30 * 60 * 1000
// 列表数据状态 // 列表数据状态
const currentCommunity = ref('请先绑定房屋') const currentCommunity = ref('请先绑定房屋')
const bannerList = ref([]) const bannerList = ref([])
@@ -342,17 +349,34 @@ const getNoticeData = () => {
}) })
.catch(() => {}) .catch(() => {})
} }
// 获取bannar图 // 获取bannar图(带缓存:30分钟内优先用缓存,请求成功后更新缓存)
function getbannarData() { function getbannarData(forceRefresh = false) {
// 非强制刷新时,优先读缓存
if (!forceRefresh) {
try {
const cached = uni.getStorageSync(BANNER_CACHE_KEY)
if (cached) {
const { data, time } = JSON.parse(cached)
if (data && data.length > 0 && Date.now() - time < BANNER_CACHE_TIME) {
bannerList.value = data
return // 缓存有效,不发起请求
}
}
} catch (e) {}
}
getBannarList() getBannarList()
.then(res => { .then(res => {
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
// 只有数据真正变化时才更新,避免 swiper 重新渲染导致闪动
const newData = res.data const newData = res.data
const oldData = bannerList.value const oldData = bannerList.value
if (JSON.stringify(newData) !== JSON.stringify(oldData)) { if (JSON.stringify(newData) !== JSON.stringify(oldData)) {
bannerList.value = newData bannerList.value = newData
} }
// 写入缓存
uni.setStorageSync(BANNER_CACHE_KEY, JSON.stringify({
data: newData,
time: Date.now()
}))
} else { } else {
bannerList.value = [] bannerList.value = []
} }
@@ -386,21 +410,26 @@ const slelctHomeData = data => {
getInspectionData() getInspectionData()
} }
const permissionlist = ref([])
onShow(() => { onShow(() => {
if (!hasInit.value) {
// 首次加载:onLoad 已触发过核心数据请求,这里只补 onLoad 没调的和需要每次都刷新的
hasInit.value = true
getbannarData() // onLoad 没调 banner
getMyOrderNum() // onLoad 没调工单数
permStore.initPerm() // 刷新权限缓存
// currentVillageData 和 getInspectionData 已在 onLoad 中调用,不再重复
return
}
// 从其他页面返回 / 从后台恢复:只刷新可能变化的数据
getbannarData() getbannarData()
getInspectionData()
currentVillageData()
getMyOrderNum() getMyOrderNum()
permStore.initPerm() // 小区和巡检数据不频繁变化,从后台恢复时用缓存/旧数据,减少请求
}) })
onLoad(() => { onLoad(() => {
uni.$on('confirm', slelctHomeData) uni.$on('confirm', slelctHomeData)
// 优先调用接口获取当前小区,失败时再读缓存
currentVillageData() currentVillageData()
getInspectionData() getInspectionData()
// getbannarData()
}) })
onUnload(() => { onUnload(() => {

View File

@@ -76,11 +76,7 @@ const onItemClick = item => {
} }
const url = routes[item.title] const url = routes[item.title]
if (!url) return if (!url) return
if (item.title === '设置') {
uni.redirectTo({ url })
} else {
uni.navigateTo({ url }) uni.navigateTo({ url })
}
} }
</script> </script>

View File

@@ -869,9 +869,11 @@ export default {
const ctx = cfu.option[cid].context; const ctx = cfu.option[cid].context;
if(typeof ctx === "object" && !!!cfu.option[cid].update){ if(typeof ctx === "object" && !!!cfu.option[cid].update){
ctx.clearRect(0, 0, this.cWidth*this.pixel, this.cHeight*this.pixel); ctx.clearRect(0, 0, this.cWidth*this.pixel, this.cHeight*this.pixel);
if (typeof ctx.draw === 'function') {
ctx.draw(); ctx.draw();
} }
} }
}
}, },
init() { init() {
let cid = this.cid let cid = this.cid

View File

@@ -4643,9 +4643,13 @@ function drawXAxis(categories, opts, config, context) {
context.beginPath(); context.beginPath();
context.setStrokeStyle(opts.xAxis.axisLineColor); context.setStrokeStyle(opts.xAxis.axisLineColor);
context.setLineWidth(1 * opts.pix); context.setLineWidth(1 * opts.pix);
if (opts.xAxis.dashLength) {
context.setLineDash([opts.xAxis.dashLength * opts.pix, opts.xAxis.dashLength * opts.pix]);
}
context.moveTo(startX, opts.height - opts.area[2]); context.moveTo(startX, opts.height - opts.area[2]);
context.lineTo(endX, opts.height - opts.area[2]); context.lineTo(endX, opts.height - opts.area[2]);
context.stroke(); context.stroke();
context.setLineDash([]);
} }
} }
@@ -5834,7 +5838,9 @@ function drawMapDataPoints(series, opts, config, context) {
mercator: mapOption.mercator mercator: mapOption.mercator
} }
drawToolTipBridge(opts, config, context, 1); drawToolTipBridge(opts, config, context, 1);
if (typeof context.draw === 'function') {
context.draw(); context.draw();
}
} }
function normalInt(min, max, iter) { function normalInt(min, max, iter) {
@@ -6305,7 +6311,9 @@ function drawCanvas(opts, context) {
context.save(); context.save();
context.translate(0, 0.5); context.translate(0, 0.5);
context.restore(); context.restore();
if (typeof context.draw === 'function') {
context.draw(); context.draw();
}
} }
var Timing = { var Timing = {