Files
property-resident-side/property-uniapp-project/pageSubPack/payment-list/paymentRecord.vue

470 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="contentStyle" style="">
<!-- <view class="status-bar"></view>
<uni-nav-bar title="缴费记录" left-icon="left" @clickLeft="goBack" backgroundColor="transparent" :border="false">
</uni-nav-bar> -->
<view class="page">
<!-- 累计缴费 -->
<view class="total-box">
<view class="top-box">
<view class="total-title">累计缴费</view>
<view class="total-price">¥{{ totalAmount }}</view>
</view>
<view class="chart-box">
<view v-if="chartData.length === 0" class="empty-state">
<uni-icons type="info" size="30" color="#ccc"></uni-icons>
<text class="empty-text">
{{ '暂无数据'}}
</text>
</view>
<!-- 循环6个月柱状图 -->
<view class="chart-item" v-for="(item,index) in chartData" :key="index">
<!-- 柱子顶部金额文字 -->
<view class="chart-label">{{ item.price }}元</view>
<!-- 柱状图柱子 -->
<view class="chart-bar" :style="{ height: barHeight(item.price) }"></view>
<!-- 底部月份文字 -->
<view class="chart-month">{{ item.month }}</view>
</view>
</view>
</view>
<!-- 缴费明细(分页滚动) -->
<scroll-view class="list-scroll" scroll-y="true" @scrolltolower="loadMore">
<view class="list-wrapper">
<!-- 按月份分组渲染 -->
<view v-if="monthGroups.length === 0" class="empty-state">
<uni-icons type="info" size="30" color="#ccc"></uni-icons>
<text class="empty-text">暂无数据</text>
</view>
<view class="month-group" v-for="(item, idx) in monthGroups" :key="idx">
<view class="month-title">
{{ item.year }}年{{ item.month }}月 缴费 ¥{{ item.total.toFixed(2) }}
</view>
<view class="item-list">
<view class="bill-item" v-for="bill in item.list" :key="bill.id">
<view class="bill-icon">
<image class="iconStyle" :src="bill.icon"></image>
</view>
<view class="bill-info">
<view class="name">{{ bill.name }}</view>
<view class="account">{{ bill.account }}</view>
</view>
<view class="bill-price">-{{ bill.amount.toFixed(2) }}</view>
</view>
</view>
</view>
<view v-if="loading" class="list-tip">加载中...</view>
<view v-else-if="noMore && monthGroups.length > 0" class="list-tip">没有更多了</view>
</view>
</scroll-view>
</view>
</view>
</template>
<script setup>
import {
ref,
onMounted,
computed
} from 'vue'
import {
onShow,
onReady,
onReachBottom,
onLoad,
onUnload,
onPullDownRefresh
} from '@dcloudio/uni-app'
import {
getTopupRecordWithStat
} from '/pageSubPack/api/apiSub.js'
// 响应式数据
const statusBarHeight = ref(20)
const pageNum = ref(1)
const pageSize = ref(10)
const totalAmount = ref('0.00')
const chartData = ref([])
const maxPrice = ref(100)
const barMaxHeight = ref(300)
const billList = ref([])
// 各月缴费合计,来自接口 monthlyStats,key 形如 "2026-09"
const monthStatsMap = ref({})
// 加载中,防止 scrolltolower 连续触发导致 pageNum 连跳
const loading = ref(false)
// 没有更多数据了
const noMore = ref(false)
// 费用名称映射
// topup(水电表充值):type 1=电费, 2=水费
// bill(物业账单):type 0=物业费, 1=车位费, 2=垃圾处理费
const getNameByType = (item) => {
if (item.recordType === 'topup') {
return item.type == '1' ? '电费' : item.type == '2' ? '水费' : '充值'
} else if (item.recordType === 'bill') {
return item.type == '0' ? '物业费' : item.type == '1' ? '车位费' : item.type == '2' ? '垃圾处理费' : '账单'
}
return ''
}
// 图标映射
const iconMap = {
'电费': 'https://wuye.ckdzkj.com/images/propertyImgs/electric.png',
'水费': 'https://wuye.ckdzkj.com/images/propertyImgs/water.png',
'物业费': 'https://wuye.ckdzkj.com/images/propertyImgs/property.png',
'车位费': 'https://wuye.ckdzkj.com/images/propertyImgs/parking.png',
'垃圾处理费': 'https://wuye.ckdzkj.com/images/propertyImgs/garbage.png',
'充值': 'https://wuye.ckdzkj.com/images/propertyImgs/water.png',
'账单': 'https://wuye.ckdzkj.com/images/propertyImgs/property.png'
}
// 获取缴费记录(loadMore=true 时分页追加)
const getRecords = async (loadMore = false) => {
if (loading.value) return
if (loadMore && noMore.value) return
loading.value = true
// 成功后才推进页码,避免请求失败时丢掉一页
const nextPage = loadMore ? pageNum.value + 1 : 1
try {
const res = await getTopupRecordWithStat({
residentUserId: uni.getStorageSync('userId'),
pageNum: nextPage,
pageSize: pageSize.value
})
if (res.code === 200) {
const records = res.data.records || []
// 转换记录格式
const mappedList = records.map(item => {
const payDate = item.payTime ? item.payTime.split(' ')[0] : ''
const parts = payDate.split('-')
const name = getNameByType(item)
return {
id: item.id,
year: parts[0] ? parseInt(parts[0], 10) : new Date().getFullYear(),
month: parts[1] ? parseInt(parts[1], 10) : 1,
name,
account: item.deviceCode || '',
amount: Number(item.paidAmount) || 0,
recordType: item.recordType || '',
icon: iconMap[name] || iconMap['账单']
}
})
pageNum.value = nextPage
// 返回条数不足一页,说明没有更多了
noMore.value = mappedList.length < pageSize.value
billList.value = loadMore ? [...billList.value, ...mappedList] : mappedList
// 累计缴费金额
totalAmount.value = res.data.totalAmount || '0.00'
// 柱状图数据(使用API返回的monthlyStats)
updateChartData(res.data.monthlyStats || {})
}
} catch (err) {
console.error('获取缴费记录失败', err)
} finally {
loading.value = false
}
}
// 加载更多(触底分页)
const loadMore = () => {
getRecords(true)
}
// 更新图表数据(使用API返回的monthlyStats)
// monthlyStats格式: { "2026-04": "04月 30.00元", "2026-05": "05月 25.00元", ... }
const updateChartData = (monthlyStats) => {
const statsMap = {}
Object.entries(monthlyStats).forEach(([key, value]) => {
const priceMatch = value.match(/([\d.]+)元/)
if (priceMatch && key.includes('-')) {
const [y, m] = key.split('-')
// 统一成补零格式(2026-09),方便和分组 key 对齐
statsMap[`${parseInt(y, 10)}-${String(parseInt(m, 10)).padStart(2, '0')}`] = {
year: parseInt(y, 10),
month: parseInt(m, 10),
price: parseFloat(priceMatch[1]) || 0
}
}
})
monthStatsMap.value = statsMap
// 取最近6个月(当前月往前)
const now = new Date()
const currentMonth = now.getMonth() + 1
const currentYear = now.getFullYear()
const months = []
for (let i = 0; i < 6; i++) {
let month = currentMonth - i
let year = currentYear
if (month <= 0) {
month += 12
year -= 1
}
const key = `${year}-${String(month).padStart(2, '0')}`
const data = statsMap[key]
months.push({
month,
year,
price: data ? data.price : 0
})
}
chartData.value = months.map((m, i) => ({
month: (m.year + '').slice(-2) + '年' + m.month + '月',
year: m.year,
sortMonth: m.year * 12 + m.month,
price: m.price,
index: i
})).sort((a, b) => a.sortMonth - b.sortMonth)
const prices = chartData.value.map(c => c.price)
maxPrice.value = Math.max(...prices, 100)
}
// 计算属性 —— 按年-月分组缴费明细
const monthGroups = computed(() => {
const map = {}
billList.value.forEach(bill => {
const key = `${bill.year}-${String(bill.month).padStart(2, '0')}`
if (!map[key]) {
map[key] = {
key,
month: bill.month,
year: bill.year,
total: 0,
list: []
}
}
map[key].list.push(bill)
map[key].total += bill.amount
})
// 分组合计优先用接口返回的月度统计:列表是分页加载的,
// 只累加已加载的记录会少算,滚动翻页时数字还会一直往上跳
const groups = Object.values(map)
groups.forEach(group => {
const stat = monthStatsMap.value[group.key]
if (stat) group.total = stat.price
})
// 按年月降序排列
return groups.sort((a, b) => {
if (a.year !== b.year) return b.year - a.year
return b.month - a.month
})
})
// 生命周期
onShow(() => {})
onMounted(() => {})
onLoad(() => {
getRecords()
})
onReady(() => {})
// 方法
const barHeight = (price) => {
const height = (price / maxPrice.value) * barMaxHeight.value
return `${height}rpx`
}
const goBack = () => {
uni.navigateBack();
}
</script>
<style lang="scss">
page {
// background: #f2f1f6;
}
</style>
<style scoped>
.contentStyle {
display: flex;
flex-direction: column;
height: 100vh;
background: linear-gradient(to left bottom, #e2efff 0%, #f9f9f9 50%);
}
.page {
flex: 1;
display: flex;
flex-direction: column;
padding: 0px 40rpx;
box-sizing: border-box;
overflow: hidden;
}
.list-scroll {
flex: 1;
overflow-y: auto;
}
.total-box {
margin-bottom: 30rpx;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
color: #ccc;
}
.empty-text {
font-size: 32rpx;
color: #999;
margin-top: 20rpx;
margin-bottom: 10rpx;
}
.list-tip {
text-align: center;
font-size: 26rpx;
color: #999;
padding: 20rpx 0 40rpx;
}
.top-box {
display: flex;
justify-content: space-between;
align-items: center;
}
.total-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.total-price {
font-size: 48rpx;
font-weight: bold;
margin-top: 10rpx;
margin-bottom: 20rpx;
}
.chart-canvas {
width: calc(100vw - 60rpx);
height: 30vh;
}
.list-wrapper {
padding: 10rpx 0rpx;
}
.month-group {
border-bottom: 2rpx solid #f2f2f2;
padding: 30rpx;
background-color: #fff;
margin-bottom: 30rpx;
border-radius: 10rpx;
}
.month-title {
font-size: 32rpx;
font-weight: 600;
margin-bottom: 30rpx;
}
.bill-item {
display: flex;
align-items: center;
margin-bottom: 30rpx;
padding-bottom: 30rpx;
border-bottom: 1px solid #ebebeb;
}
.bill-item:last-child {
border-bottom: none;
margin-bottom: 0rpx;
padding-bottom: 0rpx;
}
.bill-icon {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #4080FF;
margin-right: 24rpx;
font-size: 36rpx;
}
.iconStyle {
width: 100%;
height: 100%;
}
.bill-info {
flex: 1;
}
.name {
font-size: 28rpx;
color: #333;
font-weight: 600;
}
.account {
font-size: 24rpx;
color: #999;
margin-top: 8rpx;
}
.bill-price {
font-size: 32rpx;
color: #333;
}
.status-bar {
width: 100vw;
height: 46px;
}
</style>
<style scoped>
.chart-box {
width: 100%;
height: 444rpx;
display: flex;
align-items: flex-end;
justify-content: space-around;
box-sizing: border-box;
}
.chart-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
width: 120rpx;
font-size: 24rpx;
}
.chart-label {
font-size: 32rpx;
color: #333;
margin-bottom: 20rpx;
}
.chart-bar {
width: 40rpx;
background: #3388ff;
border-radius: 40rpx 40rpx 0 0;
}
.chart-month {
font-size: 26rpx;
color: #666;
margin-top: 24rpx;
}
</style>