AI-accounting-soft-uniApp/pages/index/index.vue

579 lines
12 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>
<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>