579 lines
12 KiB
Vue
579 lines
12 KiB
Vue
<template>
|
||
<scroll-view class="page-scroll" scroll-y="true" show-scrollbar="false">
|
||
<view class="container">
|
||
<view class="header">
|
||
<view class="header-top">
|
||
<text class="title">我的账单</text>
|
||
<view class="header-actions">
|
||
<view class="action-btn" @click="goToGoldPrice">
|
||
<text class="action-icon">🪙</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 收入支出汇总 -->
|
||
<view class="summary-row">
|
||
<view class="summary-item">
|
||
<text class="label">本月收入</text>
|
||
<text class="amount income">¥{{ monthlyIncome.toFixed(2) }}</text>
|
||
</view>
|
||
<view class="summary-item">
|
||
<text class="label">本月支出</text>
|
||
<text class="amount expense">¥{{ budget.usedAmount ? budget.usedAmount.toFixed(2) : '0.00' }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 预算信息 -->
|
||
<view class="budget-section">
|
||
<view class="budget-chart-wrapper">
|
||
<BudgetPieChart
|
||
:budget="budget.amount || 0"
|
||
:used="budget.usedAmount || 0"
|
||
chartId="mainBudgetChart"
|
||
/>
|
||
</view>
|
||
<view class="budget-stats-row">
|
||
<view class="budget-stat-item">
|
||
<text class="stat-label">本月预算</text>
|
||
<text class="stat-value">¥{{ budget.amount ? budget.amount.toFixed(2) : '0.00' }}</text>
|
||
</view>
|
||
<view class="budget-stat-item">
|
||
<text class="stat-label">每日预算</text>
|
||
<text class="stat-value">¥{{ budget.remainingDaily ? budget.remainingDaily.toFixed(2) : '0.00' }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 账单列表 -->
|
||
<view class="bill-list">
|
||
<view v-for="(group, date) in groupedBills" :key="date" class="bill-group">
|
||
<view class="group-header">
|
||
<text class="date">{{ date }}</text>
|
||
<text class="total">¥{{ getGroupTotal(group).toFixed(2) }}</text>
|
||
</view>
|
||
<view v-for="bill in group" :key="bill.id" class="bill-item" @click="viewBill(bill)">
|
||
<view class="bill-icon">{{ bill.categoryIcon || '📦' }}</view>
|
||
<view class="bill-info">
|
||
<text class="bill-category">{{ bill.categoryName || '未分类' }}</text>
|
||
<text class="bill-desc" v-if="bill.description">{{ bill.description }}</text>
|
||
</view>
|
||
<text class="bill-amount" :class="bill.amount > 0 ? 'income' : 'expense'">
|
||
{{ bill.type > 0 ? '-' : '+' }}¥{{ Math.abs(bill.amount).toFixed(2) }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="bills.length === 0 && !loading" class="empty">
|
||
<text>暂无账单记录</text>
|
||
<text class="empty-tip">点击下方"记账"按钮添加账单</text>
|
||
</view>
|
||
|
||
<view v-if="loading" class="loading">
|
||
<text>加载中...</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, onMounted } from 'vue'
|
||
import { getBills } from '../../api/bill'
|
||
import { getMonthlyStatistics } from '../../api/statistics'
|
||
import { getBudget } from '../../api/budget'
|
||
import { formatDate } from '../../utils/date'
|
||
import { useUserStore } from '../../store/user'
|
||
import BudgetPieChart from '../../components/BudgetPieChart/BudgetPieChart.vue'
|
||
|
||
const userStore = useUserStore()
|
||
const bills = ref([])
|
||
const monthlyIncome = ref(0)
|
||
const monthlyExpense = ref(0)
|
||
const budget = ref({
|
||
amount: 0,
|
||
usedAmount: 0,
|
||
remainingAmount: 0,
|
||
remainingDaily: 0
|
||
})
|
||
const loading = ref(false)
|
||
|
||
const groupedBills = computed(() => {
|
||
const groups = {}
|
||
bills.value.forEach(bill => {
|
||
const date = formatDate(bill.billDate)
|
||
if (!groups[date]) {
|
||
groups[date] = []
|
||
}
|
||
groups[date].push(bill)
|
||
})
|
||
return groups
|
||
})
|
||
|
||
const getGroupTotal = (group) => {
|
||
return group.reduce((sum, bill) => sum + parseFloat(bill.amount || 0), 0)
|
||
}
|
||
|
||
const loadBills = async () => {
|
||
loading.value = true
|
||
try {
|
||
const data = await getBills()
|
||
bills.value = data || []
|
||
} catch (error) {
|
||
console.error('加载账单失败', error)
|
||
if (error.message && error.message.includes('401')) {
|
||
// token过期,跳转到登录页
|
||
userStore.logout()
|
||
uni.reLaunch({
|
||
url: '/pages/login/login'
|
||
})
|
||
}
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
const loadMonthlyStatistics = async () => {
|
||
try {
|
||
const now = new Date()
|
||
const data = await getMonthlyStatistics(now.getFullYear(), now.getMonth() + 1)
|
||
monthlyIncome.value = data.totalIncome || 0
|
||
monthlyExpense.value = data.totalExpense || 0
|
||
} catch (error) {
|
||
console.error('加载统计失败', error)
|
||
}
|
||
}
|
||
|
||
const loadBudget = async () => {
|
||
try {
|
||
const data = await getBudget()
|
||
if (data) {
|
||
budget.value = {
|
||
amount: data.amount || 0,
|
||
usedAmount: data.usedAmount || 0,
|
||
remainingAmount: data.remainingAmount || 0,
|
||
remainingDaily: data.remainingDaily || 0
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('加载预算失败', error)
|
||
}
|
||
}
|
||
|
||
const viewBill = (bill) => {
|
||
// 跳转到账单编辑页面
|
||
uni.navigateTo({
|
||
url: `/pages/bill/detail?id=${bill.id}`
|
||
})
|
||
}
|
||
|
||
const goToAccount = () => {
|
||
// 跳转到账户管理页面
|
||
uni.navigateTo({
|
||
url: '/pages/account/account'
|
||
})
|
||
}
|
||
|
||
const goToGoldPrice = () => {
|
||
// 跳转到黄金价格页面
|
||
// 注意:在APK中路径必须与pages.json中的配置完全一致
|
||
uni.navigateTo({
|
||
url: '/pages/gold-price/gold-price',
|
||
success: () => {
|
||
console.log('跳转成功')
|
||
},
|
||
fail: (err) => {
|
||
console.error('跳转失败:', err)
|
||
// 如果navigateTo失败,尝试使用reLaunch
|
||
uni.reLaunch({
|
||
url: '/pages/gold-price/gold-price',
|
||
fail: (err2) => {
|
||
console.error('reLaunch也失败:', err2)
|
||
uni.showToast({
|
||
title: '页面跳转失败,请检查路径配置',
|
||
icon: 'none',
|
||
duration: 2000
|
||
})
|
||
}
|
||
})
|
||
}
|
||
})
|
||
}
|
||
|
||
const goToBudget = () => {
|
||
// 跳转到预算管理页面
|
||
uni.navigateTo({
|
||
url: '/pages/budget/budget'
|
||
})
|
||
}
|
||
|
||
const onPullDownRefresh = () => {
|
||
loadBills()
|
||
loadMonthlyStatistics()
|
||
loadBudget()
|
||
setTimeout(() => {
|
||
uni.stopPullDownRefresh()
|
||
}, 1000)
|
||
}
|
||
|
||
onMounted(() => {
|
||
// 检查登录状态
|
||
if (!userStore.isLoggedIn) {
|
||
uni.reLaunch({
|
||
url: '/pages/login/login'
|
||
})
|
||
return
|
||
}
|
||
|
||
loadBills()
|
||
loadMonthlyStatistics()
|
||
loadBudget()
|
||
})
|
||
|
||
// 监听刷新事件
|
||
uni.$on('refreshBills', () => {
|
||
loadBills()
|
||
loadMonthlyStatistics()
|
||
loadBudget()
|
||
})
|
||
</script>
|
||
|
||
<script>
|
||
// uni-app 页面生命周期钩子
|
||
export default {
|
||
onShow() {
|
||
// 页面显示时刷新 - 通过事件触发
|
||
uni.$emit('refreshBills')
|
||
},
|
||
onPullDownRefresh() {
|
||
// 下拉刷新 - 通过事件触发
|
||
uni.$emit('refreshBills')
|
||
setTimeout(() => {
|
||
uni.stopPullDownRefresh()
|
||
}, 1000)
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* 页面滚动容器 */
|
||
.page-scroll {
|
||
height: 100vh;
|
||
width: 100%;
|
||
}
|
||
|
||
/* 卡通风格配色 */
|
||
.container {
|
||
min-height: 100vh;
|
||
background: linear-gradient(135deg, #FFE5CC 0%, #FFD4A8 50%, #FFC08A 100%);
|
||
padding-bottom: calc(170rpx + env(safe-area-inset-bottom));
|
||
position: relative;
|
||
overflow: visible;
|
||
}
|
||
|
||
/* 背景装饰圆球 */
|
||
.container::before,
|
||
.container::after {
|
||
content: '';
|
||
position: absolute;
|
||
border-radius: 50%;
|
||
opacity: 0.2;
|
||
filter: blur(60rpx);
|
||
pointer-events: none;
|
||
}
|
||
|
||
.container::before {
|
||
width: 300rpx;
|
||
height: 300rpx;
|
||
background: linear-gradient(135deg, #C8956E, #FFB6C1);
|
||
top: 100rpx;
|
||
right: -50rpx;
|
||
animation: float 8s ease-in-out infinite;
|
||
}
|
||
|
||
.container::after {
|
||
width: 250rpx;
|
||
height: 250rpx;
|
||
background: linear-gradient(135deg, #FFD700, #F5D59E);
|
||
bottom: 200rpx;
|
||
left: -50rpx;
|
||
animation: float 10s ease-in-out infinite reverse;
|
||
}
|
||
|
||
@keyframes float {
|
||
0%, 100% { transform: translate(0, 0); }
|
||
50% { transform: translate(-20rpx, 30rpx); }
|
||
}
|
||
|
||
.header {
|
||
background: linear-gradient(135deg, #C8956E 0%, #F5D59E 50%, #FFD700 100%);
|
||
padding: 40rpx 30rpx;
|
||
color: #5D4037;
|
||
border-radius: 0 0 40rpx 40rpx;
|
||
box-shadow: 0 10rpx 30rpx rgba(93, 64, 55, 0.15);
|
||
border: 3rpx solid #5D4037;
|
||
border-top: none;
|
||
position: relative;
|
||
z-index: 10;
|
||
}
|
||
|
||
.header-top {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 30rpx;
|
||
}
|
||
|
||
.title {
|
||
font-size: 44rpx;
|
||
font-weight: bold;
|
||
display: block;
|
||
text-shadow:
|
||
2rpx 2rpx 0 #FFD700,
|
||
-1rpx -1rpx 0 #FFD700,
|
||
0 4rpx 10rpx rgba(255, 215, 0, 0.3);
|
||
}
|
||
|
||
.header-actions {
|
||
display: flex;
|
||
gap: 15rpx;
|
||
}
|
||
|
||
.action-btn {
|
||
width: 70rpx;
|
||
height: 70rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: rgba(255, 255, 255, 0.9);
|
||
border-radius: 50%;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
border: 3rpx solid #5D4037;
|
||
box-shadow: 0 4rpx 15rpx rgba(93, 64, 55, 0.2);
|
||
}
|
||
|
||
.action-btn:active {
|
||
background: #fff;
|
||
transform: scale(0.9);
|
||
box-shadow: 0 2rpx 8rpx rgba(93, 64, 55, 0.15);
|
||
}
|
||
|
||
.action-icon {
|
||
font-size: 40rpx;
|
||
filter: drop-shadow(0 2rpx 4rpx rgba(0, 0, 0, 0.1));
|
||
}
|
||
|
||
.summary-row {
|
||
display: flex;
|
||
justify-content: space-around;
|
||
margin-bottom: 25rpx;
|
||
}
|
||
|
||
.summary-item {
|
||
text-align: center;
|
||
flex: 1;
|
||
}
|
||
|
||
.label {
|
||
display: block;
|
||
font-size: 26rpx;
|
||
opacity: 0.85;
|
||
margin-bottom: 10rpx;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.amount {
|
||
display: block;
|
||
font-size: 40rpx;
|
||
font-weight: bold;
|
||
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.amount.income {
|
||
color: #19BE6B;
|
||
}
|
||
|
||
.amount.expense {
|
||
color: #FA3534;
|
||
}
|
||
|
||
.budget-section {
|
||
background: rgba(255, 255, 255, 0.8);
|
||
backdrop-filter: blur(10rpx);
|
||
border-radius: 20rpx;
|
||
padding: 30rpx 20rpx 20rpx;
|
||
border: 3rpx solid #5D4037;
|
||
box-shadow: 0 8rpx 20rpx rgba(93, 64, 55, 0.15);
|
||
}
|
||
|
||
.budget-chart-wrapper {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
margin-bottom: 25rpx;
|
||
}
|
||
|
||
.budget-stats-row {
|
||
display: flex;
|
||
justify-content: space-around;
|
||
gap: 20rpx;
|
||
}
|
||
|
||
.budget-stat-item {
|
||
flex: 1;
|
||
text-align: center;
|
||
background: linear-gradient(135deg, rgba(200, 149, 110, 0.2), rgba(255, 215, 0, 0.2));
|
||
padding: 20rpx 15rpx;
|
||
border-radius: 15rpx;
|
||
border: 2rpx solid #C8956E;
|
||
}
|
||
|
||
.stat-label {
|
||
display: block;
|
||
font-size: 24rpx;
|
||
opacity: 0.85;
|
||
margin-bottom: 10rpx;
|
||
color: #5D4037;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.stat-value {
|
||
display: block;
|
||
font-size: 32rpx;
|
||
font-weight: bold;
|
||
color: #5D4037;
|
||
}
|
||
|
||
.bill-list {
|
||
padding: 20rpx;
|
||
}
|
||
|
||
.bill-group {
|
||
background: rgba(255, 255, 255, 0.95);
|
||
border-radius: 20rpx;
|
||
margin-bottom: 20rpx;
|
||
overflow: hidden;
|
||
border: 3rpx solid #C8956E;
|
||
box-shadow: 0 8rpx 20rpx rgba(200, 149, 110, 0.15);
|
||
animation: slideIn 0.3s ease-out;
|
||
}
|
||
|
||
@keyframes slideIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(20rpx);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
.group-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 20rpx 30rpx;
|
||
background: linear-gradient(135deg, rgba(200, 149, 110, 0.15), rgba(255, 215, 0, 0.15));
|
||
border-bottom: 2rpx solid #FFD700;
|
||
}
|
||
|
||
.date {
|
||
font-size: 28rpx;
|
||
color: #5D4037;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.total {
|
||
font-size: 30rpx;
|
||
font-weight: bold;
|
||
color: #5D4037;
|
||
}
|
||
|
||
.bill-item {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 30rpx;
|
||
border-bottom: 1rpx solid rgba(200, 149, 110, 0.2);
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.bill-item:active {
|
||
background: rgba(255, 215, 0, 0.1);
|
||
}
|
||
|
||
.bill-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.bill-icon {
|
||
font-size: 52rpx;
|
||
margin-right: 20rpx;
|
||
filter: drop-shadow(0 2rpx 6rpx rgba(0, 0, 0, 0.15));
|
||
}
|
||
|
||
.bill-info {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.bill-category {
|
||
font-size: 30rpx;
|
||
color: #5D4037;
|
||
margin-bottom: 8rpx;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.bill-desc {
|
||
font-size: 24rpx;
|
||
color: #8D6E63;
|
||
}
|
||
|
||
.bill-amount {
|
||
font-size: 34rpx;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.bill-amount.income {
|
||
color: #19BE6B;
|
||
}
|
||
|
||
.bill-amount.expense {
|
||
color: #FA3534;
|
||
}
|
||
|
||
.empty {
|
||
text-align: center;
|
||
padding: 120rpx 0;
|
||
color: #8D6E63;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
background: rgba(255, 255, 255, 0.8);
|
||
border-radius: 20rpx;
|
||
margin: 20rpx;
|
||
border: 3rpx dashed #C8956E;
|
||
}
|
||
|
||
.empty text:first-child {
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
color: #5D4037;
|
||
}
|
||
|
||
.empty-tip {
|
||
font-size: 26rpx;
|
||
margin-top: 20rpx;
|
||
color: #BCAAA4;
|
||
}
|
||
|
||
.loading {
|
||
text-align: center;
|
||
padding: 40rpx 0;
|
||
color: #8D6E63;
|
||
font-size: 28rpx;
|
||
}
|
||
</style>
|