Files
account/resources/mobile/pages/index/index.vue
2026-01-19 12:29:33 +08:00

847 lines
18 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>
<un-pages
:show-nav-bar="true"
nav-bar-title="首页"
:show-back="false"
:show-tab-bar="true"
@tab-change="handleTabChange"
>
<view class="page-content">
<!-- 用户信息头部 -->
<view class="user-header">
<view class="user-avatar">
<text>{{ userName ? userName.charAt(0) : 'U' }}</text>
</view>
<view class="user-info">
<text class="greeting">你好{{ userName || '用户' }}</text>
<text class="date-text">{{ currentDate }}</text>
</view>
</view>
<!-- 收支概览卡片 -->
<view class="overview-card">
<view class="overview-header">
<text class="overview-title">本月概览</text>
<text class="overview-month">{{ currentMonth }}</text>
</view>
<view class="overview-body">
<view class="overview-item income">
<view class="overview-icon">
<text class="icon-text"></text>
</view>
<view class="overview-data">
<text class="overview-label">收入</text>
<text class="overview-amount">¥{{ overview.income.toFixed(2) }}</text>
</view>
</view>
<view class="overview-divider"></view>
<view class="overview-item expense">
<view class="overview-icon">
<text class="icon-text"></text>
</view>
<view class="overview-data">
<text class="overview-label">支出</text>
<text class="overview-amount">¥{{ overview.expense.toFixed(2) }}</text>
</view>
</view>
</view>
</view>
<!-- 快捷操作 -->
<view class="quick-actions">
<view class="action-card" @tap="navigateTo('/pages/account/bill/add')">
<view class="action-icon-wrapper">
<uni-icons type="plus" size="36" color="#fff"></uni-icons>
</view>
<text class="action-title">记一笔</text>
<text class="action-desc">快速记录</text>
</view>
<view class="action-card" @tap="navigateTo('/pages/account/bill/index')">
<view class="action-icon-wrapper">
<uni-icons type="list" size="36" color="#fff"></uni-icons>
</view>
<text class="action-title">账单</text>
<text class="action-desc">查看明细</text>
</view>
<view class="action-card" @tap="navigateTo('/pages/account/statistics/index')">
<view class="action-icon-wrapper">
<uni-icons type="color-filled" size="36" color="#fff"></uni-icons>
</view>
<text class="action-title">统计</text>
<text class="action-desc">数据分析</text>
</view>
</view>
<!-- 最近记录 -->
<view class="recent-section">
<view class="section-header">
<text class="section-title">最近记录</text>
<view class="section-more" @tap="navigateTo('/pages/account/bill/index')">
<text>全部</text>
<uni-icons type="right" size="14" color="#667eea"></uni-icons>
</view>
</view>
<view v-if="loading" class="loading-state">
<uni-load-more status="loading"></uni-load-more>
</view>
<view v-else-if="recentBills.length === 0" class="empty-list">
<view class="empty-icon">
<uni-icons type="list" size="80" color="#ddd"></uni-icons>
</view>
<text class="empty-text">暂无记录</text>
<button class="quick-add-btn" @tap="navigateTo('/pages/account/bill/add')">
<uni-icons type="plus" size="16" color="#fff"></uni-icons>
<text>记一笔</text>
</button>
</view>
<view v-else class="bill-list">
<view class="bill-item" v-for="bill in recentBills" :key="bill.id" @tap="viewBill(bill)">
<view class="bill-icon" :style="{background: getCategoryColor(bill.category_id)}">
<text>{{ getCategoryIcon(bill.category_id) }}</text>
</view>
<view class="bill-info">
<text class="bill-category">{{ getCategoryName(bill.category_id) }}</text>
<text class="bill-date">{{ formatDate(bill.date, 'MM/dd') }}</text>
</view>
<view class="bill-amount" :class="bill.type">
<text>{{ bill.type === 'expense' ? '-' : '+' }}¥{{ bill.amount.toFixed(2) }}</text>
</view>
</view>
</view>
</view>
<!-- 家庭管理 -->
<view class="family-section">
<view class="section-header">
<text class="section-title">家庭管理</text>
<view class="section-more" @tap="navigateTo('/pages/family/index')">
<text>管理</text>
<uni-icons type="right" size="14" color="#667eea"></uni-icons>
</view>
</view>
<view class="family-card" @tap="navigateTo('/pages/family/index')">
<view class="family-icon-wrapper">
<text class="family-icon-text">🏠</text>
</view>
<view class="family-info">
<text class="family-status">{{ hasFamily ? '已加入家庭' : '未加入家庭' }}</text>
<text class="family-desc">{{ hasFamily ? '点击查看家庭成员' : '点击创建或加入家庭' }}</text>
</view>
<view class="family-arrow">
<uni-icons type="right" size="18" color="#ccc"></uni-icons>
</view>
</view>
</view>
</view>
</un-pages>
</template>
<script>
import tool from '@/utils/tool'
export default {
data() {
return {
currentDate: '',
currentMonth: '',
overview: {
income: 0,
expense: 0
},
recentBills: [],
loading: false,
categoryMap: {
1: { name: '餐饮', icon: '🍜', color: '#FF6B6B' },
2: { name: '交通', icon: '🚗', color: '#4ECDC4' },
3: { name: '购物', icon: '🛒', color: '#45B7D1' },
4: { name: '娱乐', icon: '🎮', color: '#96CEB4' },
5: { name: '医疗', icon: '💊', color: '#FFEAA7' },
6: { name: '教育', icon: '📚', color: '#DDA0DD' },
7: { name: '居住', icon: '🏠', color: '#98D8C8' },
8: { name: '其他', icon: '📦', color: '#BDC3C7' },
101: { name: '工资', icon: '💰', color: '#2ECC71' },
102: { name: '奖金', icon: '🎁', color: '#E74C3C' },
103: { name: '投资', icon: '📈', color: '#3498DB' },
104: { name: '兼职', icon: '💼', color: '#9B59B6' },
105: { name: '其他', icon: '💎', color: '#1ABC9C' }
}
}
},
computed: {
hasFamily() {
if (!this.$store || !this.$store.getters) {
return false
}
return this.$store.getters.hasFamily || false
},
userName() {
if (!this.$store || !this.$store.state || !this.$store.state.user || !this.$store.state.user.userInfo) {
return ''
}
return this.$store.state.user.userInfo.name || this.$store.state.user.userInfo.username || ''
}
},
onLoad() {
this.updateDate()
this.loadFamilyInfo()
this.loadData()
},
onShow() {
this.updateDate()
this.loadFamilyInfo()
this.loadData()
},
onPullDownRefresh() {
this.loadData().then(() => {
uni.stopPullDownRefresh()
})
},
methods: {
updateDate() {
const now = new Date()
const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
const month = now.getMonth() + 1
const date = now.getDate()
const day = weekDays[now.getDay()]
this.currentDate = `${month}${date}${day}`
this.currentMonth = `${month}`
},
async loadFamilyInfo() {
try {
await this.$store.dispatch('getFamilyInfo')
} catch (error) {
console.error('获取家庭信息失败', error)
}
},
async loadData() {
this.loading = true
try {
const now = new Date()
const year = now.getFullYear()
const month = now.getMonth() + 1
// 并行加载数据
const [overviewRes, billsRes] = await Promise.all([
this.$api.statistics.overview.get({ year, month }),
this.$api.bill.list.get({ year, month, limit: 5 })
])
if (overviewRes && overviewRes.code === 1) {
this.overview = overviewRes.data || { income: 0, expense: 0 }
}
if (billsRes && billsRes.code === 1) {
this.recentBills = billsRes.data?.list || []
}
} catch (error) {
console.error('加载数据失败', error)
} finally {
this.loading = false
}
},
handleTabChange(path) {
console.log('Tab changed to:', path)
},
navigateTo(url) {
uni.navigateTo({
url: url
})
},
viewBill(bill) {
// TODO: 实现查看账单详情
uni.navigateTo({
url: `/pages/account/bill/detail?id=${bill.id}`
})
},
getCategoryName(categoryId) {
return this.categoryMap[categoryId]?.name || '未知'
},
getCategoryIcon(categoryId) {
return this.categoryMap[categoryId]?.icon || '📦'
},
getCategoryColor(categoryId) {
return this.categoryMap[categoryId]?.color || '#BDC3C7'
},
formatDate(date, fmt) {
return tool.dateFormat(date, fmt)
}
}
}
</script>
<style lang="scss" scoped>
.page-content {
padding: 30rpx;
background: linear-gradient(180deg, #F5F7FA 0%, #FFFFFF 100%);
position: relative;
&::before {
content: '';
position: absolute;
top: -100rpx;
right: -100rpx;
width: 300rpx;
height: 300rpx;
background: radial-gradient(circle, rgba(102, 126, 234, 0.1) 0%, transparent 70%);
border-radius: 50%;
z-index: 0;
}
&::after {
content: '';
position: absolute;
bottom: 200rpx;
left: -100rpx;
width: 250rpx;
height: 250rpx;
background: radial-gradient(circle, rgba(118, 75, 162, 0.08) 0%, transparent 70%);
border-radius: 50%;
z-index: 0;
}
> view {
position: relative;
z-index: 1;
}
}
.user-header {
display: flex;
align-items: center;
padding: 20rpx 0 40rpx;
animation: fadeInDown 0.6s ease-out;
.user-avatar {
width: 88rpx;
height: 88rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 28rpx;
box-shadow: 0 8rpx 20rpx rgba(102, 126, 234, 0.35);
position: relative;
&::before {
content: '';
position: absolute;
top: -4rpx;
right: -4rpx;
width: 20rpx;
height: 20rpx;
background: #4cd964;
border-radius: 50%;
border: 3rpx solid #fff;
}
text {
font-size: 36rpx;
color: #fff;
font-weight: bold;
}
}
.user-info {
flex: 1;
display: flex;
flex-direction: column;
.greeting {
font-size: 36rpx;
font-weight: bold;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 8rpx;
}
.date-text {
font-size: 26rpx;
color: #999;
font-weight: 500;
}
}
}
.overview-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 24rpx;
padding: 40rpx 32rpx;
margin-bottom: 40rpx;
box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.4);
position: relative;
overflow: hidden;
animation: fadeInUp 0.6s ease-out 0.1s both;
&::before {
content: '';
position: absolute;
top: -50%;
right: -50%;
width: 100%;
height: 100%;
background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%);
}
&::after {
content: '';
position: absolute;
bottom: -30%;
left: -30%;
width: 80%;
height: 80%;
background: radial-gradient(circle, rgba(255, 255, 255, 0.05) 0%, transparent 70%);
}
.overview-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32rpx;
position: relative;
z-index: 1;
.overview-title {
font-size: 30rpx;
color: rgba(255, 255, 255, 0.95);
font-weight: 600;
}
.overview-month {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.85);
background: rgba(255, 255, 255, 0.25);
padding: 8rpx 20rpx;
border-radius: 24rpx;
backdrop-filter: blur(10rpx);
}
}
.overview-body {
display: flex;
align-items: center;
position: relative;
z-index: 1;
.overview-item {
flex: 1;
display: flex;
align-items: center;
transition: transform 0.3s ease;
&:active {
transform: scale(0.98);
}
&.income {
.overview-icon {
background: rgba(46, 204, 113, 0.25);
box-shadow: 0 4rpx 12rpx rgba(46, 204, 113, 0.3);
}
.overview-amount {
background: linear-gradient(135deg, #98D8C8 0%, #7FDBDA 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
}
&.expense {
.overview-icon {
background: rgba(255, 107, 107, 0.25);
box-shadow: 0 4rpx 12rpx rgba(255, 107, 107, 0.3);
}
.overview-amount {
background: linear-gradient(135deg, #FFEAA7 0%, #FD79A8 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
}
.overview-icon {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
transition: transform 0.3s ease;
.icon-text {
font-size: 26rpx;
color: #fff;
font-weight: bold;
}
}
.overview-data {
flex: 1;
display: flex;
flex-direction: column;
.overview-label {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.8);
margin-bottom: 8rpx;
font-weight: 500;
}
.overview-amount {
font-size: 36rpx;
font-weight: bold;
letter-spacing: 0.5rpx;
}
}
}
.overview-divider {
width: 2rpx;
height: 72rpx;
background: rgba(255, 255, 255, 0.3);
margin: 0 32rpx;
}
}
}
.quick-actions {
display: flex;
justify-content: space-between;
margin-bottom: 40rpx;
animation: fadeInUp 0.6s ease-out 0.2s both;
.action-card {
flex: 1;
background: #fff;
border-radius: 24rpx;
padding: 40rpx 16rpx;
margin: 0 8rpx;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.08);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4rpx;
opacity: 0;
transition: opacity 0.3s ease;
}
&:active {
transform: translateY(-4rpx) scale(0.98);
box-shadow: 0 12rpx 28rpx rgba(0, 0, 0, 0.12);
.action-icon-wrapper {
transform: scale(1.1);
}
}
&:nth-child(1) {
.action-icon-wrapper {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
&::before {
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
}
}
&:nth-child(2) {
.action-icon-wrapper {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
&::before {
background: linear-gradient(90deg, #f093fb 0%, #f5576c 100%);
}
}
&:nth-child(3) {
.action-icon-wrapper {
background: linear-gradient(135deg, #4ECDC4 0%, #44A08D 100%);
}
&::before {
background: linear-gradient(90deg, #4ECDC4 0%, #44A08D 100%);
}
}
.action-icon-wrapper {
width: 88rpx;
height: 88rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20rpx;
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.15);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.action-title {
font-size: 30rpx;
color: #333;
font-weight: bold;
margin-bottom: 8rpx;
}
.action-desc {
font-size: 24rpx;
color: #999;
font-weight: 500;
}
}
}
.recent-section,
.family-section {
background: #fff;
border-radius: 24rpx;
padding: 32rpx;
margin-bottom: 30rpx;
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.08);
animation: fadeInUp 0.6s ease-out both;
}
.recent-section {
animation-delay: 0.3s;
}
.family-section {
animation-delay: 0.4s;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32rpx;
.section-title {
font-size: 30rpx;
font-weight: bold;
background: linear-gradient(135deg, #333 0%, #666 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.section-more {
display: flex;
align-items: center;
font-size: 26rpx;
color: #667eea;
font-weight: 500;
padding: 8rpx 16rpx;
border-radius: 20rpx;
background: rgba(102, 126, 234, 0.08);
transition: all 0.3s ease;
&:active {
background: rgba(102, 126, 234, 0.15);
}
text {
margin-right: 6rpx;
}
}
}
.loading-state {
padding: 60rpx 0;
}
.empty-list {
display: flex;
flex-direction: column;
align-items: center;
padding: 60rpx 0;
.empty-icon {
margin-bottom: 20rpx;
opacity: 0.5;
}
.empty-text {
font-size: 26rpx;
color: #999;
margin-bottom: 30rpx;
}
.quick-add-btn {
display: flex;
align-items: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
border: none;
border-radius: 50rpx;
padding: 16rpx 36rpx;
font-size: 26rpx;
box-shadow: 0 4rpx 12rpx rgba(102, 126, 234, 0.3);
transition: all 0.3s ease;
&:active {
transform: scale(0.95);
box-shadow: 0 6rpx 16rpx rgba(102, 126, 234, 0.4);
}
uni-icons {
margin-right: 8rpx;
}
&::after {
border: none;
}
}
}
.bill-list {
.bill-item {
display: flex;
align-items: center;
padding: 24rpx 0;
border-bottom: 2rpx solid #f5f5f5;
&:last-child {
border-bottom: none;
}
.bill-icon {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
text {
font-size: 28rpx;
}
}
.bill-info {
flex: 1;
display: flex;
flex-direction: column;
.bill-category {
font-size: 28rpx;
color: #333;
font-weight: bold;
margin-bottom: 6rpx;
}
.bill-date {
font-size: 22rpx;
color: #999;
}
}
.bill-amount {
font-size: 28rpx;
font-weight: bold;
&.expense {
color: #FF6B6B;
}
&.income {
color: #2ECC71;
}
}
}
}
.family-card {
display: flex;
align-items: center;
padding: 12rpx 0;
transition: transform 0.3s ease;
&:active {
transform: translateX(4rpx);
}
.family-icon-wrapper {
width: 80rpx;
height: 80rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 24rpx;
box-shadow: 0 6rpx 16rpx rgba(102, 126, 234, 0.3);
transition: all 0.3s ease;
.family-icon-text {
font-size: 40rpx;
}
}
.family-info {
flex: 1;
display: flex;
flex-direction: column;
.family-status {
font-size: 30rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
}
.family-desc {
font-size: 24rpx;
color: #999;
font-weight: 500;
}
}
.family-arrow {
padding: 8rpx;
}
}
// 动画关键帧
@keyframes fadeInDown {
from {
opacity: 0;
transform: translateY(-30rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>