1078 lines
28 KiB
Vue
1078 lines
28 KiB
Vue
<template>
|
|
<view class="stats-page">
|
|
<!-- 自定义导航栏 -->
|
|
<view class="custom-nav safe-area-top">
|
|
<view class="nav-content">
|
|
<text class="nav-title">收支统计</text>
|
|
<view class="nav-actions">
|
|
<view class="nav-btn" @click="showDatePicker">
|
|
<text class="date-text">{{currentPeriod}}</text>
|
|
<uni-icons type="down" size="14" color="#64748B"></uni-icons>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<scroll-view scroll-y class="page-content" @scrolltolower="loadMore">
|
|
<!-- 统计概览卡片 - 对应Web端统计概览 -->
|
|
<view class="overview-section">
|
|
<view class="overview-row">
|
|
<view class="overview-card income">
|
|
<view class="card-icon">
|
|
<uni-icons type="wallet-filled" size="28" color="#FFFFFF"></uni-icons>
|
|
</view>
|
|
<view class="card-content">
|
|
<text class="card-value">¥{{formatAmount(statistics.totalReceivable)}}</text>
|
|
<text class="card-label">应收总额</text>
|
|
</view>
|
|
</view>
|
|
<view class="overview-card received">
|
|
<view class="card-icon">
|
|
<uni-icons type="checkmarkempty" size="28" color="#FFFFFF"></uni-icons>
|
|
</view>
|
|
<view class="card-content">
|
|
<text class="card-value">¥{{formatAmount(statistics.totalReceived)}}</text>
|
|
<text class="card-label">实收总额</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
<view class="overview-row">
|
|
<view class="overview-card pending">
|
|
<view class="card-icon">
|
|
<uni-icons type="time-filled" size="28" color="#FFFFFF"></uni-icons>
|
|
</view>
|
|
<view class="card-content">
|
|
<text class="card-value">¥{{formatAmount(statistics.totalUnreceived)}}</text>
|
|
<text class="card-label">待收金额</text>
|
|
</view>
|
|
</view>
|
|
<view class="overview-card expense">
|
|
<view class="card-icon">
|
|
<uni-icons type="cart-filled" size="28" color="#FFFFFF"></uni-icons>
|
|
</view>
|
|
<view class="card-content">
|
|
<text class="card-value">¥{{formatAmount(statistics.totalExpense)}}</text>
|
|
<text class="card-label">总支出</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 收支趋势图 - 对应Web端收支趋势 -->
|
|
<view class="chart-section">
|
|
<view class="section-header">
|
|
<text class="section-title">收支趋势</text>
|
|
</view>
|
|
<view class="chart-card">
|
|
<view class="trend-chart">
|
|
<view
|
|
v-for="(item, index) in trendData"
|
|
:key="index"
|
|
class="trend-bar-group"
|
|
>
|
|
<view class="trend-bars">
|
|
<view class="bar-wrapper">
|
|
<view
|
|
class="trend-bar income"
|
|
:style="{ height: item.incomeHeight + 'rpx' }"
|
|
></view>
|
|
</view>
|
|
<view class="bar-wrapper">
|
|
<view
|
|
class="trend-bar expense"
|
|
:style="{ height: item.expenseHeight + 'rpx' }"
|
|
></view>
|
|
</view>
|
|
</view>
|
|
<text class="trend-label">{{item.label}}</text>
|
|
</view>
|
|
</view>
|
|
<view class="chart-legend">
|
|
<view class="legend-item">
|
|
<view class="legend-dot income"></view>
|
|
<text>收入</text>
|
|
</view>
|
|
<view class="legend-item">
|
|
<view class="legend-dot expense"></view>
|
|
<text>支出</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 收入构成 - 对应Web端收入构成饼图 -->
|
|
<view class="composition-section">
|
|
<view class="section-header">
|
|
<text class="section-title">收入构成</text>
|
|
</view>
|
|
<view class="composition-card">
|
|
<view class="composition-list">
|
|
<view
|
|
v-for="(item, index) in incomeByCategory"
|
|
:key="index"
|
|
class="composition-item"
|
|
>
|
|
<view class="item-left">
|
|
<view class="item-icon" :style="{ background: getCategoryColor(item.category) }">
|
|
<text class="icon-text">{{getCategoryIcon(item.category)}}</text>
|
|
</view>
|
|
<view class="item-info">
|
|
<text class="item-name">{{getCategoryText(item.category)}}</text>
|
|
<text class="item-percent">{{item.percent}}%</text>
|
|
</view>
|
|
</view>
|
|
<view class="item-right">
|
|
<text class="item-amount">¥{{formatAmount(item.amount)}}</text>
|
|
<view class="progress-bar">
|
|
<view
|
|
class="progress-fill"
|
|
:style="{ width: item.percent + '%', background: getCategoryColor(item.category) }"
|
|
></view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
<view v-if="incomeByCategory.length === 0" class="empty-tip">
|
|
<text>暂无收入数据</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 支出构成 - 对应Web端支出构成饼图 -->
|
|
<view class="composition-section">
|
|
<view class="section-header">
|
|
<text class="section-title">支出构成</text>
|
|
</view>
|
|
<view class="composition-card">
|
|
<view class="composition-list">
|
|
<view
|
|
v-for="(item, index) in expenseByCategory"
|
|
:key="index"
|
|
class="composition-item"
|
|
>
|
|
<view class="item-left">
|
|
<view class="item-icon" :style="{ background: getCategoryColor(item.category) }">
|
|
<text class="icon-text">{{getCategoryIcon(item.category)}}</text>
|
|
</view>
|
|
<view class="item-info">
|
|
<text class="item-name">{{getCategoryText(item.category)}}</text>
|
|
<text class="item-percent">{{item.percent}}%</text>
|
|
</view>
|
|
</view>
|
|
<view class="item-right">
|
|
<text class="item-amount expense">-¥{{formatAmount(item.amount)}}</text>
|
|
<view class="progress-bar">
|
|
<view
|
|
class="progress-fill"
|
|
:style="{ width: item.percent + '%', background: getCategoryColor(item.category) }"
|
|
></view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
<view v-if="expenseByCategory.length === 0" class="empty-tip">
|
|
<text>暂无支出数据</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 收支明细 - 对应Web端详细数据表格 -->
|
|
<view class="detail-section">
|
|
<view class="section-header">
|
|
<text class="section-title">收支明细</text>
|
|
</view>
|
|
<view class="detail-tabs">
|
|
<view
|
|
v-for="(tab, index) in detailTabs"
|
|
:key="index"
|
|
class="detail-tab"
|
|
:class="{ active: activeTab === tab.value }"
|
|
@click="switchTab(tab.value)"
|
|
>
|
|
<text>{{tab.label}}</text>
|
|
</view>
|
|
</view>
|
|
<view class="detail-list">
|
|
<view
|
|
v-for="(item, index) in currentDetailList"
|
|
:key="index"
|
|
class="detail-item"
|
|
@click="viewDetail(item)"
|
|
>
|
|
<view class="detail-left">
|
|
<view class="detail-icon" :class="item.type">
|
|
<uni-icons
|
|
:type="item.type === 'income' ? 'wallet-filled' : 'cart-filled'"
|
|
size="20"
|
|
color="#FFFFFF"
|
|
></uni-icons>
|
|
</view>
|
|
<view class="detail-info">
|
|
<text class="detail-title">{{getCategoryText(item.category)}}</text>
|
|
<text class="detail-subtitle">{{item.roomNumber}} · {{item.renterName || '-'}}</text>
|
|
<text class="detail-date">{{item.billDate}}</text>
|
|
</view>
|
|
</view>
|
|
<view class="detail-right">
|
|
<text class="detail-amount" :class="item.type">
|
|
{{item.type === 'income' ? '+' : '-'}}¥{{formatAmount(item.receivableAmount)}}
|
|
</text>
|
|
<view class="detail-status" :class="item.status">
|
|
<text>{{getStatusText(item.status)}}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
<view v-if="currentDetailList.length === 0" class="empty-tip">
|
|
<text>暂无明细数据</text>
|
|
</view>
|
|
</view>
|
|
<view v-if="hasMore" class="load-more" @click="loadMore">
|
|
<text>加载更多</text>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="safe-area-bottom" style="height: 40rpx;"></view>
|
|
</scroll-view>
|
|
</view>
|
|
</template>
|
|
|
|
<script>
|
|
import { billApi, settingApi } from '../../api/index.js'
|
|
|
|
export default {
|
|
data() {
|
|
return {
|
|
currentPeriod: '本月',
|
|
dateRange: [],
|
|
statistics: {
|
|
totalReceivable: 0,
|
|
totalReceived: 0,
|
|
totalUnreceived: 0,
|
|
totalExpense: 0
|
|
},
|
|
trendData: [],
|
|
incomeByCategory: [],
|
|
expenseByCategory: [],
|
|
incomeList: [],
|
|
expenseList: [],
|
|
activeTab: 'income',
|
|
detailTabs: [
|
|
{ label: '收入明细', value: 'income' },
|
|
{ label: '支出明细', value: 'expense' }
|
|
],
|
|
categoryMap: {},
|
|
isLoading: false,
|
|
page: 1,
|
|
pageSize: 10,
|
|
hasMore: true
|
|
}
|
|
},
|
|
computed: {
|
|
currentDetailList() {
|
|
return this.activeTab === 'income' ? this.incomeList : this.expenseList
|
|
}
|
|
},
|
|
onLoad() {
|
|
this.initDateRange()
|
|
this.loadCategories()
|
|
this.loadData()
|
|
},
|
|
onPullDownRefresh() {
|
|
this.page = 1
|
|
this.hasMore = true
|
|
this.loadData().finally(() => {
|
|
uni.stopPullDownRefresh()
|
|
})
|
|
},
|
|
methods: {
|
|
// 初始化日期范围为当月
|
|
initDateRange() {
|
|
const now = new Date()
|
|
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1)
|
|
const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0)
|
|
this.dateRange = [
|
|
this.formatDate(startOfMonth),
|
|
this.formatDate(endOfMonth)
|
|
]
|
|
},
|
|
|
|
// 加载类目
|
|
async loadCategories() {
|
|
try {
|
|
const response = await settingApi.getCategories()
|
|
const categories = response.data || []
|
|
const map = {}
|
|
categories.forEach(cat => {
|
|
map[cat.code] = cat.name
|
|
})
|
|
this.categoryMap = map
|
|
} catch (error) {
|
|
console.error('加载类目列表失败:', error)
|
|
}
|
|
},
|
|
|
|
// 加载数据
|
|
async loadData() {
|
|
if (this.isLoading) return
|
|
this.isLoading = true
|
|
|
|
try {
|
|
const billRes = await billApi.list({
|
|
startDate: this.dateRange[0],
|
|
endDate: this.dateRange[1],
|
|
page: this.page,
|
|
pageSize: this.pageSize
|
|
})
|
|
|
|
this.processData(billRes.data || [])
|
|
} catch (error) {
|
|
uni.showToast({
|
|
title: '加载数据失败',
|
|
icon: 'none'
|
|
})
|
|
} finally {
|
|
this.isLoading = false
|
|
}
|
|
},
|
|
|
|
// 处理数据
|
|
processData(bills) {
|
|
let totalReceivable = 0
|
|
let totalReceived = 0
|
|
let totalExpense = 0
|
|
const incomeList = []
|
|
const expenseList = []
|
|
const incomeByCategory = {}
|
|
const expenseByCategory = {}
|
|
const trendData = {}
|
|
|
|
bills.forEach(bill => {
|
|
const receivable = parseFloat(bill.receivableAmount) || 0
|
|
const received = parseFloat(bill.receivedAmount) || 0
|
|
const billDate = bill.billDate || (bill.createTime && bill.createTime.split(' ')[0])
|
|
|
|
if (bill.type === 'income') {
|
|
totalReceivable += receivable
|
|
totalReceived += received
|
|
|
|
// 按类别统计收入
|
|
const category = bill.category || 'other'
|
|
if (!incomeByCategory[category]) {
|
|
incomeByCategory[category] = 0
|
|
}
|
|
incomeByCategory[category] += receivable
|
|
|
|
// 按日期统计趋势
|
|
if (billDate) {
|
|
if (!trendData[billDate]) {
|
|
trendData[billDate] = { income: 0, expense: 0 }
|
|
}
|
|
trendData[billDate].income += receivable
|
|
}
|
|
|
|
incomeList.push({
|
|
...bill,
|
|
type: 'income',
|
|
roomNumber: bill.roomNumber || (bill.Room && bill.Room.roomNumber) || '-',
|
|
renterName: bill.renterName || (bill.Renter && bill.Renter.name) || '-'
|
|
})
|
|
} else if (bill.type === 'expense') {
|
|
totalExpense += receivable
|
|
|
|
// 按类别统计支出
|
|
const category = bill.category || 'other'
|
|
if (!expenseByCategory[category]) {
|
|
expenseByCategory[category] = 0
|
|
}
|
|
expenseByCategory[category] += receivable
|
|
|
|
// 按日期统计趋势
|
|
if (billDate) {
|
|
if (!trendData[billDate]) {
|
|
trendData[billDate] = { income: 0, expense: 0 }
|
|
}
|
|
trendData[billDate].expense += receivable
|
|
}
|
|
|
|
expenseList.push({
|
|
...bill,
|
|
type: 'expense',
|
|
roomNumber: bill.roomNumber || (bill.Room && bill.Room.roomNumber) || '-'
|
|
})
|
|
}
|
|
})
|
|
|
|
// 按日期排序
|
|
incomeList.sort((a, b) => new Date(b.billDate) - new Date(a.billDate))
|
|
expenseList.sort((a, b) => new Date(b.billDate) - new Date(a.billDate))
|
|
|
|
this.statistics = {
|
|
totalReceivable,
|
|
totalReceived,
|
|
totalUnreceived: totalReceivable - totalReceived,
|
|
totalExpense
|
|
}
|
|
|
|
// 处理收入分类数据
|
|
const incomeTotal = Object.values(incomeByCategory).reduce((sum, val) => sum + val, 0)
|
|
this.incomeByCategory = Object.keys(incomeByCategory).map(category => ({
|
|
category,
|
|
amount: incomeByCategory[category],
|
|
percent: incomeTotal > 0 ? ((incomeByCategory[category] / incomeTotal) * 100).toFixed(1) : 0
|
|
})).sort((a, b) => b.amount - a.amount)
|
|
|
|
// 处理支出分类数据
|
|
const expenseTotal = Object.values(expenseByCategory).reduce((sum, val) => sum + val, 0)
|
|
this.expenseByCategory = Object.keys(expenseByCategory).map(category => ({
|
|
category,
|
|
amount: expenseByCategory[category],
|
|
percent: expenseTotal > 0 ? ((expenseByCategory[category] / expenseTotal) * 100).toFixed(1) : 0
|
|
})).sort((a, b) => b.amount - a.amount)
|
|
|
|
// 处理趋势数据
|
|
this.processTrendData(trendData)
|
|
|
|
if (this.page === 1) {
|
|
this.incomeList = incomeList
|
|
this.expenseList = expenseList
|
|
} else {
|
|
this.incomeList = [...this.incomeList, ...incomeList]
|
|
this.expenseList = [...this.expenseList, ...expenseList]
|
|
}
|
|
|
|
this.hasMore = bills.length >= this.pageSize
|
|
},
|
|
|
|
// 处理趋势数据
|
|
processTrendData(trendData) {
|
|
const dates = Object.keys(trendData).sort()
|
|
const maxIncome = Math.max(...dates.map(d => trendData[d].income), 1)
|
|
const maxExpense = Math.max(...dates.map(d => trendData[d].expense), 1)
|
|
const maxValue = Math.max(maxIncome, maxExpense)
|
|
|
|
// 只取最近7天的数据
|
|
const recentDates = dates.slice(-7)
|
|
|
|
this.trendData = recentDates.map(date => {
|
|
const data = trendData[date]
|
|
return {
|
|
label: date.slice(5), // 显示 MM-DD
|
|
incomeHeight: maxValue > 0 ? (data.income / maxValue) * 200 : 0,
|
|
expenseHeight: maxValue > 0 ? (data.expense / maxValue) * 200 : 0
|
|
}
|
|
})
|
|
},
|
|
|
|
// 显示日期选择
|
|
showDatePicker() {
|
|
uni.showActionSheet({
|
|
itemList: ['本周', '本月', '本季度', '本年', '自定义'],
|
|
success: (res) => {
|
|
const periods = ['本周', '本月', '本季度', '本年', '自定义']
|
|
this.currentPeriod = periods[res.tapIndex]
|
|
this.updateDateRange(res.tapIndex)
|
|
}
|
|
})
|
|
},
|
|
|
|
// 更新日期范围
|
|
updateDateRange(index) {
|
|
const now = new Date()
|
|
let start, end
|
|
|
|
switch(index) {
|
|
case 0: // 本周
|
|
const dayOfWeek = now.getDay() || 7
|
|
start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - dayOfWeek + 1)
|
|
end = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
|
break
|
|
case 1: // 本月
|
|
start = new Date(now.getFullYear(), now.getMonth(), 1)
|
|
end = new Date(now.getFullYear(), now.getMonth() + 1, 0)
|
|
break
|
|
case 2: // 本季度
|
|
const quarter = Math.floor(now.getMonth() / 3)
|
|
start = new Date(now.getFullYear(), quarter * 3, 1)
|
|
end = new Date(now.getFullYear(), quarter * 3 + 3, 0)
|
|
break
|
|
case 3: // 本年
|
|
start = new Date(now.getFullYear(), 0, 1)
|
|
end = new Date(now.getFullYear(), 11, 31)
|
|
break
|
|
default:
|
|
return
|
|
}
|
|
|
|
this.dateRange = [this.formatDate(start), this.formatDate(end)]
|
|
this.page = 1
|
|
this.hasMore = true
|
|
this.loadData()
|
|
},
|
|
|
|
// 切换明细标签
|
|
switchTab(value) {
|
|
this.activeTab = value
|
|
},
|
|
|
|
// 加载更多
|
|
loadMore() {
|
|
if (this.hasMore && !this.isLoading) {
|
|
this.page++
|
|
this.loadData()
|
|
}
|
|
},
|
|
|
|
// 查看详情
|
|
viewDetail(item) {
|
|
uni.navigateTo({
|
|
url: `/pages/bill-detail/bill-detail?id=${item.id}`
|
|
})
|
|
},
|
|
|
|
// 格式化日期
|
|
formatDate(date) {
|
|
if (!date) return '-'
|
|
const d = new Date(date)
|
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
|
|
},
|
|
|
|
// 格式化金额
|
|
formatAmount(amount) {
|
|
if (amount === null || amount === undefined) return '0.00'
|
|
return parseFloat(amount).toFixed(2)
|
|
},
|
|
|
|
// 获取类目文本
|
|
getCategoryText(category) {
|
|
const texts = {
|
|
'rent': '租金',
|
|
'deposit': '押金',
|
|
'water': '水费',
|
|
'electricity': '电费',
|
|
'maintenance': '维修费',
|
|
'property': '物业费',
|
|
'other': '其他'
|
|
}
|
|
return this.categoryMap[category] || texts[category] || category || '其他'
|
|
},
|
|
|
|
// 获取类目图标
|
|
getCategoryIcon(category) {
|
|
const icons = {
|
|
'rent': '租',
|
|
'deposit': '押',
|
|
'water': '水',
|
|
'electricity': '电',
|
|
'maintenance': '修',
|
|
'property': '物',
|
|
'other': '其'
|
|
}
|
|
return icons[category] || '账'
|
|
},
|
|
|
|
// 获取类目颜色
|
|
getCategoryColor(category) {
|
|
const colors = {
|
|
'rent': '#667eea',
|
|
'deposit': '#f093fb',
|
|
'water': '#4facfe',
|
|
'electricity': '#f5576c',
|
|
'maintenance': '#fa709a',
|
|
'property': '#30cfd0',
|
|
'other': '#a8edea'
|
|
}
|
|
return colors[category] || '#667eea'
|
|
},
|
|
|
|
// 获取状态文本
|
|
getStatusText(status) {
|
|
const texts = {
|
|
'paid': '已付清',
|
|
'partial': '部分付款',
|
|
'unpaid': '未付款'
|
|
}
|
|
return texts[status] || status
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.stats-page {
|
|
min-height: 100vh;
|
|
background: #F8FAFC;
|
|
display: flex;
|
|
flex-direction: column;
|
|
width: 100%;
|
|
max-width: 100vw;
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
/* 自定义导航栏 */
|
|
.custom-nav {
|
|
background: #FFFFFF;
|
|
border-bottom: 2rpx solid #F1F5F9;
|
|
width: 100%;
|
|
}
|
|
|
|
.nav-content {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 20rpx 32rpx;
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.nav-title {
|
|
font-size: 36rpx;
|
|
font-weight: 700;
|
|
color: #1E293B;
|
|
}
|
|
|
|
.nav-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.nav-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8rpx;
|
|
padding: 12rpx 24rpx;
|
|
background: #F8FAFC;
|
|
border-radius: 12rpx;
|
|
}
|
|
|
|
.date-text {
|
|
font-size: 26rpx;
|
|
color: #64748B;
|
|
}
|
|
|
|
/* 页面内容 */
|
|
.page-content {
|
|
flex: 1;
|
|
padding: 24rpx 32rpx;
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
/* 统计概览 */
|
|
.overview-section {
|
|
margin-bottom: 32rpx;
|
|
}
|
|
|
|
.overview-row {
|
|
display: flex;
|
|
gap: 24rpx;
|
|
margin-bottom: 24rpx;
|
|
}
|
|
|
|
.overview-row:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.overview-card {
|
|
flex: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 28rpx;
|
|
border-radius: 20rpx;
|
|
color: #FFFFFF;
|
|
}
|
|
|
|
.overview-card.income {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
}
|
|
|
|
.overview-card.received {
|
|
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
|
}
|
|
|
|
.overview-card.pending {
|
|
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
|
|
}
|
|
|
|
.overview-card.expense {
|
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
|
}
|
|
|
|
.card-icon {
|
|
width: 64rpx;
|
|
height: 64rpx;
|
|
border-radius: 16rpx;
|
|
background: rgba(255, 255, 255, 0.2);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin-right: 20rpx;
|
|
}
|
|
|
|
.card-content {
|
|
flex: 1;
|
|
}
|
|
|
|
.card-value {
|
|
display: block;
|
|
font-size: 32rpx;
|
|
font-weight: 700;
|
|
margin-bottom: 8rpx;
|
|
}
|
|
|
|
.card-label {
|
|
font-size: 24rpx;
|
|
opacity: 0.9;
|
|
}
|
|
|
|
/* 区域标题 */
|
|
.section-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 24rpx;
|
|
}
|
|
|
|
.section-title {
|
|
font-size: 32rpx;
|
|
font-weight: 700;
|
|
color: #1E293B;
|
|
}
|
|
|
|
/* 图表区域 */
|
|
.chart-section {
|
|
margin-bottom: 32rpx;
|
|
}
|
|
|
|
.chart-card {
|
|
background: #FFFFFF;
|
|
border-radius: 24rpx;
|
|
padding: 32rpx;
|
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04);
|
|
}
|
|
|
|
.trend-chart {
|
|
display: flex;
|
|
justify-content: space-around;
|
|
align-items: flex-end;
|
|
height: 280rpx;
|
|
padding-bottom: 40rpx;
|
|
border-bottom: 2rpx solid #F1F5F9;
|
|
margin-bottom: 24rpx;
|
|
}
|
|
|
|
.trend-bar-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 16rpx;
|
|
}
|
|
|
|
.trend-bars {
|
|
display: flex;
|
|
gap: 8rpx;
|
|
align-items: flex-end;
|
|
height: 220rpx;
|
|
}
|
|
|
|
.bar-wrapper {
|
|
display: flex;
|
|
align-items: flex-end;
|
|
height: 220rpx;
|
|
}
|
|
|
|
.trend-bar {
|
|
width: 28rpx;
|
|
border-radius: 6rpx;
|
|
transition: height 0.3s ease;
|
|
}
|
|
|
|
.trend-bar.income {
|
|
background: linear-gradient(180deg, #667eea 0%, #764ba2 100%);
|
|
}
|
|
|
|
.trend-bar.expense {
|
|
background: linear-gradient(180deg, #f093fb 0%, #f5576c 100%);
|
|
}
|
|
|
|
.trend-label {
|
|
font-size: 22rpx;
|
|
color: #64748B;
|
|
}
|
|
|
|
.chart-legend {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 48rpx;
|
|
}
|
|
|
|
.legend-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12rpx;
|
|
font-size: 26rpx;
|
|
color: #64748B;
|
|
}
|
|
|
|
.legend-dot {
|
|
width: 16rpx;
|
|
height: 16rpx;
|
|
border-radius: 4rpx;
|
|
}
|
|
|
|
.legend-dot.income {
|
|
background: linear-gradient(180deg, #667eea 0%, #764ba2 100%);
|
|
}
|
|
|
|
.legend-dot.expense {
|
|
background: linear-gradient(180deg, #f093fb 0%, #f5576c 100%);
|
|
}
|
|
|
|
/* 构成区域 */
|
|
.composition-section {
|
|
margin-bottom: 32rpx;
|
|
}
|
|
|
|
.composition-card {
|
|
background: #FFFFFF;
|
|
border-radius: 24rpx;
|
|
padding: 24rpx 32rpx;
|
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04);
|
|
}
|
|
|
|
.composition-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.composition-item {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 20rpx 0;
|
|
border-bottom: 2rpx solid #F1F5F9;
|
|
}
|
|
|
|
.composition-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.item-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 20rpx;
|
|
}
|
|
|
|
.item-icon {
|
|
width: 56rpx;
|
|
height: 56rpx;
|
|
border-radius: 14rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.icon-text {
|
|
font-size: 24rpx;
|
|
color: #FFFFFF;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.item-info {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4rpx;
|
|
}
|
|
|
|
.item-name {
|
|
font-size: 28rpx;
|
|
color: #1E293B;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.item-percent {
|
|
font-size: 22rpx;
|
|
color: #94A3B8;
|
|
}
|
|
|
|
.item-right {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-end;
|
|
gap: 8rpx;
|
|
}
|
|
|
|
.item-amount {
|
|
font-size: 28rpx;
|
|
font-weight: 600;
|
|
color: #1E293B;
|
|
}
|
|
|
|
.item-amount.expense {
|
|
color: #f5576c;
|
|
}
|
|
|
|
.progress-bar {
|
|
width: 120rpx;
|
|
height: 6rpx;
|
|
background: #F1F5F9;
|
|
border-radius: 3rpx;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.progress-fill {
|
|
height: 100%;
|
|
border-radius: 3rpx;
|
|
transition: width 0.3s ease;
|
|
}
|
|
|
|
/* 明细区域 */
|
|
.detail-section {
|
|
margin-bottom: 32rpx;
|
|
}
|
|
|
|
.detail-tabs {
|
|
display: flex;
|
|
background: #FFFFFF;
|
|
border-radius: 16rpx;
|
|
padding: 8rpx;
|
|
margin-bottom: 24rpx;
|
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04);
|
|
}
|
|
|
|
.detail-tab {
|
|
flex: 1;
|
|
text-align: center;
|
|
padding: 20rpx 0;
|
|
font-size: 28rpx;
|
|
color: #64748B;
|
|
border-radius: 12rpx;
|
|
}
|
|
|
|
.detail-tab.active {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: #FFFFFF;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.detail-list {
|
|
background: #FFFFFF;
|
|
border-radius: 24rpx;
|
|
padding: 0 32rpx;
|
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04);
|
|
}
|
|
|
|
.detail-item {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 28rpx 0;
|
|
border-bottom: 2rpx solid #F1F5F9;
|
|
}
|
|
|
|
.detail-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.detail-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 20rpx;
|
|
}
|
|
|
|
.detail-icon {
|
|
width: 72rpx;
|
|
height: 72rpx;
|
|
border-radius: 18rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.detail-icon.income {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
}
|
|
|
|
.detail-icon.expense {
|
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
|
}
|
|
|
|
.detail-info {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6rpx;
|
|
}
|
|
|
|
.detail-title {
|
|
font-size: 30rpx;
|
|
font-weight: 600;
|
|
color: #1E293B;
|
|
}
|
|
|
|
.detail-subtitle {
|
|
font-size: 24rpx;
|
|
color: #64748B;
|
|
}
|
|
|
|
.detail-date {
|
|
font-size: 22rpx;
|
|
color: #94A3B8;
|
|
}
|
|
|
|
.detail-right {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-end;
|
|
gap: 8rpx;
|
|
}
|
|
|
|
.detail-amount {
|
|
font-size: 32rpx;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.detail-amount.income {
|
|
color: #667eea;
|
|
}
|
|
|
|
.detail-amount.expense {
|
|
color: #f5576c;
|
|
}
|
|
|
|
.detail-status {
|
|
padding: 6rpx 16rpx;
|
|
border-radius: 8rpx;
|
|
font-size: 22rpx;
|
|
}
|
|
|
|
.detail-status.paid {
|
|
background: #DCFCE7;
|
|
color: #16A34A;
|
|
}
|
|
|
|
.detail-status.partial {
|
|
background: #FEF3C7;
|
|
color: #D97706;
|
|
}
|
|
|
|
.detail-status.unpaid {
|
|
background: #FEE2E2;
|
|
color: #DC2626;
|
|
}
|
|
|
|
/* 空提示 */
|
|
.empty-tip {
|
|
text-align: center;
|
|
padding: 60rpx 0;
|
|
color: #94A3B8;
|
|
font-size: 28rpx;
|
|
}
|
|
|
|
/* 加载更多 */
|
|
.load-more {
|
|
text-align: center;
|
|
padding: 32rpx;
|
|
color: #64748B;
|
|
font-size: 26rpx;
|
|
}
|
|
</style>
|