Files
property-resident-side/property-uniapp-project/pageSubPack/payment-list/paymentRecord.vue
2026-08-24 17:04:18 +08:00

437 lines
9.9 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>
</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([])
// 费用名称映射
// 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) => {
try {
if (!loadMore) pageNum.value = 1
const res = await getTopupRecordWithStat({
residentUserId: uni.getStorageSync('userId'),
pageNum: pageNum.value,
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.amount) || 0,
recordType: item.recordType || '',
icon: iconMap[name] || iconMap['账单']
}
})
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)
}
}
// 加载更多(触底分页)
const loadMore = () => {
pageNum.value++
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('-')
statsMap[key] = {
year: parseInt(y, 10),
month: parseInt(m, 10),
price: parseFloat(priceMatch[1]) || 0
}
}
})
// 取最近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] = {
month: bill.month,
year: bill.year,
total: 0,
list: []
}
}
map[key].list.push(bill)
map[key].total += bill.amount
})
// 按年月降序排列
return Object.values(map).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;
}
.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>