329 lines
7.6 KiB
Vue
329 lines
7.6 KiB
Vue
<template>
|
|
<view class="container">
|
|
<view class="tabs">
|
|
<view class="tab" :class="{ active: activeTab === 'daily' }" @click="switchTab('daily')">
|
|
按日
|
|
</view>
|
|
<view class="tab" :class="{ active: activeTab === 'weekly' }" @click="switchTab('weekly')">
|
|
按周
|
|
</view>
|
|
<view class="tab" :class="{ active: activeTab === 'monthly' }" @click="switchTab('monthly')">
|
|
按月
|
|
</view>
|
|
</view>
|
|
|
|
<view class="summary-card">
|
|
<view class="summary-item">
|
|
<text class="label">总收入</text>
|
|
<text class="amount income">¥{{ statistics.totalIncome?.toFixed(2) || '0.00' }}</text>
|
|
</view>
|
|
<view class="summary-item">
|
|
<text class="label">总支出</text>
|
|
<text class="amount expense">¥{{ statistics.totalExpense?.toFixed(2) || '0.00' }}</text>
|
|
</view>
|
|
<view class="summary-item">
|
|
<text class="label">余额</text>
|
|
<text class="amount balance">¥{{ getBalance.toFixed(2) }}</text>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="category-stats">
|
|
<text class="section-title">分类统计</text>
|
|
<view v-if="statistics.categoryStatistics && statistics.categoryStatistics.length > 0">
|
|
<view v-for="item in statistics.categoryStatistics" :key="item.categoryId" class="category-item">
|
|
<text class="category-icon">{{ item.categoryIcon || '📦' }}</text>
|
|
<view class="category-info">
|
|
<text class="category-name">{{ item.categoryName }}</text>
|
|
<view class="progress-bar">
|
|
<view class="progress-fill" :style="{ width: getProgress(item) + '%' }"></view>
|
|
</view>
|
|
</view>
|
|
<text class="category-amount">¥{{ item.amount?.toFixed(2) || '0.00' }}</text>
|
|
</view>
|
|
</view>
|
|
<view v-else class="empty">
|
|
<text>暂无统计数据</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, onMounted } from 'vue'
|
|
import { getDailyStatistics, getWeeklyStatistics, getMonthlyStatistics } from '../../api/statistics'
|
|
import { formatDate, getWeekStart } from '../../utils/date'
|
|
|
|
const activeTab = ref('monthly')
|
|
const statistics = ref({
|
|
totalIncome: 0,
|
|
totalExpense: 0,
|
|
balance: 0,
|
|
categoryStatistics: [],
|
|
dailyStatistics: []
|
|
})
|
|
|
|
const switchTab = async (tab) => {
|
|
activeTab.value = tab
|
|
await loadStatistics()
|
|
}
|
|
|
|
const loadStatistics = async () => {
|
|
try {
|
|
const now = new Date()
|
|
let data
|
|
|
|
if (activeTab.value === 'daily') {
|
|
const startDate = formatDate(now)
|
|
const endDate = formatDate(now)
|
|
data = await getDailyStatistics(startDate, endDate)
|
|
} else if (activeTab.value === 'weekly') {
|
|
const weekStart = formatDate(getWeekStart(now))
|
|
data = await getWeeklyStatistics(weekStart)
|
|
} else {
|
|
data = await getMonthlyStatistics(now.getFullYear(), now.getMonth() + 1)
|
|
}
|
|
|
|
const income = data?.totalIncome || 0
|
|
const expense = data?.totalExpense || 0
|
|
statistics.value = {
|
|
totalIncome: income,
|
|
totalExpense: expense,
|
|
balance: income - expense,
|
|
categoryStatistics: data?.categoryStatistics || [],
|
|
dailyStatistics: data?.dailyStatistics || []
|
|
}
|
|
} catch (error) {
|
|
console.error('加载统计失败', error)
|
|
statistics.value = {
|
|
totalIncome: 0,
|
|
totalExpense: 0,
|
|
balance: 0,
|
|
categoryStatistics: [],
|
|
dailyStatistics: []
|
|
}
|
|
}
|
|
}
|
|
|
|
const getBalance = computed(() => {
|
|
return statistics.value.totalIncome - statistics.value.totalExpense
|
|
})
|
|
|
|
const getProgress = (item) => {
|
|
const total = statistics.value.totalExpense || 1
|
|
const amount = parseFloat(item.amount || 0)
|
|
return total > 0 ? (amount / total) * 100 : 0
|
|
}
|
|
|
|
onMounted(() => {
|
|
loadStatistics()
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.container {
|
|
min-height: calc(100vh - 100rpx);
|
|
background: linear-gradient(135deg, #FFE5CC 0%, #FFD4A8 50%, #FFC08A 100%);
|
|
padding: 20rpx;
|
|
padding-bottom: calc(170rpx + env(safe-area-inset-bottom));
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* 背景装饰 */
|
|
.container::before {
|
|
content: '';
|
|
position: absolute;
|
|
width: 280rpx;
|
|
height: 280rpx;
|
|
background: linear-gradient(135deg, #C8956E, #FFB6C1);
|
|
border-radius: 50%;
|
|
opacity: 0.2;
|
|
filter: blur(60rpx);
|
|
top: 150rpx;
|
|
left: -80rpx;
|
|
animation: float 10s ease-in-out infinite;
|
|
pointer-events: none;
|
|
}
|
|
|
|
@keyframes float {
|
|
0%, 100% { transform: translate(0, 0); }
|
|
50% { transform: translate(-15rpx, 25rpx); }
|
|
}
|
|
|
|
.tabs {
|
|
display: flex;
|
|
background: rgba(255, 255, 255, 0.9);
|
|
border-radius: 20rpx;
|
|
margin-bottom: 20rpx;
|
|
padding: 10rpx;
|
|
border: 3rpx solid #C8956E;
|
|
box-shadow: 0 8rpx 20rpx rgba(200, 149, 110, 0.15);
|
|
}
|
|
|
|
.tab {
|
|
flex: 1;
|
|
text-align: center;
|
|
padding: 20rpx;
|
|
border-radius: 15rpx;
|
|
color: #8D6E63;
|
|
font-size: 28rpx;
|
|
font-weight: 600;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.tab.active {
|
|
background: linear-gradient(135deg, #C8956E, #F5D59E);
|
|
color: #5D4037;
|
|
box-shadow: 0 4rpx 15rpx rgba(200, 149, 110, 0.3);
|
|
}
|
|
|
|
.summary-card {
|
|
background: linear-gradient(135deg, #C8956E 0%, #F5D59E 50%, #FFD700 100%);
|
|
border-radius: 25rpx;
|
|
padding: 50rpx 40rpx;
|
|
margin-bottom: 20rpx;
|
|
display: flex;
|
|
justify-content: space-around;
|
|
border: 4rpx solid #5D4037;
|
|
box-shadow: 0 15rpx 40rpx rgba(93, 64, 55, 0.2);
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.summary-card::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: -50%;
|
|
right: -50%;
|
|
width: 200%;
|
|
height: 200%;
|
|
background: radial-gradient(circle, rgba(255, 255, 255, 0.2) 0%, transparent 70%);
|
|
pointer-events: none;
|
|
}
|
|
|
|
.summary-item {
|
|
text-align: center;
|
|
color: #5D4037;
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
.label {
|
|
display: block;
|
|
font-size: 26rpx;
|
|
opacity: 0.85;
|
|
margin-bottom: 12rpx;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.amount {
|
|
display: block;
|
|
font-size: 40rpx;
|
|
font-weight: bold;
|
|
text-shadow: 2rpx 2rpx 0 rgba(255, 255, 255, 0.3);
|
|
}
|
|
|
|
.amount.income {
|
|
color: #19BE6B;
|
|
}
|
|
|
|
.amount.expense {
|
|
color: #FA3534;
|
|
}
|
|
|
|
.amount.balance {
|
|
color: #5D4037;
|
|
}
|
|
|
|
.category-stats {
|
|
background: rgba(255, 255, 255, 0.95);
|
|
border-radius: 25rpx;
|
|
padding: 40rpx 30rpx;
|
|
border: 3rpx solid #C8956E;
|
|
box-shadow: 0 10rpx 30rpx rgba(200, 149, 110, 0.15);
|
|
}
|
|
|
|
.section-title {
|
|
display: block;
|
|
font-size: 34rpx;
|
|
font-weight: bold;
|
|
margin-bottom: 35rpx;
|
|
color: #5D4037;
|
|
text-shadow: 2rpx 2rpx 0 rgba(255, 215, 0, 0.2);
|
|
}
|
|
|
|
.category-item {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 25rpx 0;
|
|
border-bottom: 2rpx solid rgba(200, 149, 110, 0.2);
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.category-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.category-item:active {
|
|
background: rgba(255, 215, 0, 0.05);
|
|
padding-left: 10rpx;
|
|
}
|
|
|
|
.category-icon {
|
|
font-size: 56rpx;
|
|
margin-right: 25rpx;
|
|
filter: drop-shadow(0 3rpx 6rpx rgba(0, 0, 0, 0.15));
|
|
}
|
|
|
|
.category-info {
|
|
flex: 1;
|
|
}
|
|
|
|
.category-name {
|
|
display: block;
|
|
font-size: 30rpx;
|
|
color: #5D4037;
|
|
margin-bottom: 12rpx;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.progress-bar {
|
|
width: 100%;
|
|
height: 12rpx;
|
|
background: rgba(200, 149, 110, 0.2);
|
|
border-radius: 20rpx;
|
|
overflow: hidden;
|
|
box-shadow: inset 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.progress-fill {
|
|
height: 100%;
|
|
background: linear-gradient(90deg, #C8956E, #FFD700);
|
|
transition: width 0.5s ease-out;
|
|
border-radius: 20rpx;
|
|
box-shadow: 0 0 10rpx rgba(255, 215, 0, 0.5);
|
|
}
|
|
|
|
.category-amount {
|
|
font-size: 32rpx;
|
|
font-weight: bold;
|
|
color: #5D4037;
|
|
margin-left: 25rpx;
|
|
}
|
|
|
|
.empty {
|
|
text-align: center;
|
|
padding: 100rpx 0;
|
|
color: #8D6E63;
|
|
background: rgba(255, 255, 255, 0.5);
|
|
border-radius: 20rpx;
|
|
border: 3rpx dashed #C8956E;
|
|
margin: 20rpx 0;
|
|
}
|
|
|
|
.empty text {
|
|
font-size: 30rpx;
|
|
font-weight: 600;
|
|
}
|
|
</style>
|