Files
EmailBill/Web/src/views/StatisticsView.vue
孙诚 840e6dbac9
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 15s
Docker Build & Deploy / Deploy to Production (push) Successful in 5s
添加功能
2025-12-27 22:34:19 +08:00

1172 lines
29 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>
<div class="page-container-flex">
<!-- 顶部导航栏 -->
<van-nav-bar title="账单统计" placeholder>
<template #right>
<van-icon name="chat-o" size="20" @click="goToAnalysis" />
</template>
</van-nav-bar>
<!-- 月份选择器 固定区域 -->
<div class="sticky-header">
<van-button
icon="arrow-left"
plain
size="small"
@click="changeMonth(-1)"
/>
<div class="sticky-header-text" @click="showMonthPicker = true">
{{ currentYear }}{{ currentMonth }}
<van-icon name="arrow-down" />
</div>
<van-button
icon="arrow"
plain
size="small"
@click="changeMonth(1)"
:disabled="isCurrentMonth"
/>
</div>
<!-- 下拉刷新 -->
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<!-- 加载中 -->
<van-loading v-if="loading" vertical style="padding: 100px 0">
加载统计数据中...
</van-loading>
<!-- 统计内容 -->
<div v-else class="statistics-content">
<!-- 月度概览卡片 -->
<div class="overview-card">
<div class="overview-item clickable" @click="goToTypeOverviewBills(0)">
<div class="label">总支出</div>
<div class="value expense">¥{{ formatMoney(monthlyData.totalExpense) }}</div>
<div class="sub-text">{{ monthlyData.expenseCount }}</div>
</div>
<div class="divider"></div>
<div class="overview-item clickable" @click="goToTypeOverviewBills(1)">
<div class="label">总收入</div>
<div class="value income">¥{{ formatMoney(monthlyData.totalIncome) }}</div>
<div class="sub-text">{{ monthlyData.incomeCount }}</div>
</div>
<div class="divider"></div>
<div class="overview-item clickable" @click="goToTypeOverviewBills(null)">
<div class="label">结余</div>
<div class="value" :class="monthlyData.balance >= 0 ? 'income' : 'expense'">
{{ monthlyData.balance >= 0 ? '' : '-' }}¥{{ formatMoney(Math.abs(monthlyData.balance)) }}
</div>
<div class="sub-text">{{ monthlyData.totalCount }}笔交易</div>
</div>
</div>
<!-- 分类统计 -->
<div class="common-card">
<div class="card-header">
<h3 class="card-title">支出分类统计</h3>
<van-tag type="primary" size="medium">{{ expenseCategories.length }}</van-tag>
</div>
<!-- 环形图区域 -->
<div class="chart-container" v-if="expenseCategories.length > 0">
<div class="ring-chart">
<svg viewBox="0 0 200 200" class="ring-svg">
<circle
v-for="(segment, index) in chartSegments"
:key="index"
cx="100"
cy="100"
r="70"
fill="none"
:stroke="segment.color"
:stroke-width="35"
:stroke-dasharray="`${segment.length} ${circumference - segment.length}`"
:stroke-dashoffset="-segment.offset"
transform="rotate(-90 100 100)"
class="ring-segment"
/>
</svg>
<div class="ring-center">
<div class="center-value">¥{{ formatMoney(monthlyData.totalExpense) }}</div>
<div class="center-label">总支出</div>
</div>
</div>
</div>
<!-- 分类列表 -->
<div class="category-list" v-if="expenseCategories.length > 0">
<div
v-for="(category) in expenseCategories"
:key="category.classify"
class="category-item clickable"
@click="goToCategoryBills(category.classify, 0)"
>
<div class="category-info">
<div class="category-color" :style="{ backgroundColor: category.color }"></div>
<div class="category-name-with-count">
<span class="category-name">{{ category.classify || '未分类' }}</span>
<span class="category-count">{{ category.count }}</span>
</div>
</div>
<div class="category-stats">
<div class="category-amount">¥{{ formatMoney(category.amount) }}</div>
<div class="category-percent">{{ category.percent }}%</div>
</div>
<van-icon name="arrow" class="category-arrow" />
</div>
</div>
<van-empty
v-else
description="本月暂无支出记录"
image="search"
/>
</div>
<!-- 收入分类统计 -->
<div class="common-card" v-if="incomeCategories.length > 0">
<div class="card-header">
<h3 class="card-title">收入分类统计</h3>
<van-tag type="success" size="medium">{{ incomeCategories.length }}</van-tag>
</div>
<div class="category-list">
<div
v-for="category in incomeCategories"
:key="category.classify"
class="category-item clickable"
@click="goToCategoryBills(category.classify, 1)"
>
<div class="category-info">
<div class="category-color income-color"></div>
<div class="category-name-with-count">
<span class="category-name">{{ category.classify || '未分类' }}</span>
<span class="category-count">{{ category.count }}</span>
</div>
</div>
<div class="category-stats">
<div class="category-amount income-text">¥{{ formatMoney(category.amount) }}</div>
<div class="category-percent">{{ category.percent }}%</div>
</div>
<van-icon name="arrow" class="category-arrow" />
</div>
</div>
</div>
<!-- 不计收支分类统计 -->
<div class="common-card" v-if="noneCategories.length > 0">
<div class="card-header">
<h3 class="card-title">不计收支分类统计</h3>
<van-tag type="info" size="medium">{{ noneCategories.length }}</van-tag>
</div>
<div class="category-list">
<div
v-for="category in noneCategories"
:key="category.classify"
class="category-item clickable"
@click="goToCategoryBills(category.classify, 2)"
>
<div class="category-info">
<div class="category-color none-color"></div>
<div class="category-name-with-count">
<span class="category-name">{{ category.classify || '未分类' }}</span>
<span class="category-count">{{ category.count }}</span>
</div>
</div>
<div class="category-stats">
<div class="category-amount none-text">¥{{ formatMoney(category.amount) }}</div>
<div class="category-percent">{{ category.percent }}%</div>
</div>
<van-icon name="arrow" class="category-arrow" />
</div>
</div>
</div>
<!-- 趋势统计 -->
<div class="common-card">
<div class="card-header">
<h3 class="card-title">近6个月趋势</h3>
</div>
<div class="trend-chart">
<div class="trend-bars">
<div
v-for="item in trendData"
:key="item.month"
class="trend-bar-group"
>
<div class="bar-container">
<div
class="bar expense-bar"
:style="{ height: getBarHeight(item.expense, maxTrendValue) }"
>
<div class="bar-value" v-if="item.expense > 0">
{{ formatShortMoney(item.expense) }}
</div>
</div>
<div
class="bar income-bar"
:style="{ height: getBarHeight(item.income, maxTrendValue) }"
>
<div class="bar-value" v-if="item.income > 0">
{{ formatShortMoney(item.income) }}
</div>
</div>
</div>
<div class="bar-label">{{ item.label }}</div>
</div>
</div>
<div class="trend-legend">
<div class="legend-item">
<div class="legend-color expense-color"></div>
<span>支出</span>
</div>
<div class="legend-item">
<div class="legend-color income-color"></div>
<span>收入</span>
</div>
</div>
</div>
</div>
<!-- 其他统计 -->
<div class="common-card">
<div class="card-header">
<h3 class="card-title">其他统计</h3>
</div>
<div class="other-stats">
<div class="stat-item">
<div class="stat-label">日均支出</div>
<div class="stat-value">¥{{ formatMoney(dailyAverage.expense) }}</div>
</div>
<div class="stat-item">
<div class="stat-label">日均收入</div>
<div class="stat-value income-text">¥{{ formatMoney(dailyAverage.income) }}</div>
</div>
<div class="stat-item">
<div class="stat-label">最大单笔支出</div>
<div class="stat-value">¥{{ formatMoney(monthlyData.maxExpense) }}</div>
</div>
<div class="stat-item">
<div class="stat-label">最大单笔收入</div>
<div class="stat-value income-text">¥{{ formatMoney(monthlyData.maxIncome) }}</div>
</div>
</div>
</div>
<!-- 底部安全距离 -->
<div style="height: calc(50px + env(safe-area-inset-bottom, 0px))"></div>
</div>
</van-pull-refresh>
<!-- 月份选择器 -->
<van-popup v-model:show="showMonthPicker" position="bottom" round>
<van-date-picker
v-model="selectedDate"
title="选择月份"
:min-date="minDate"
:max-date="maxDate"
:columns-type="['year', 'month']"
@confirm="onMonthConfirm"
@cancel="showMonthPicker = false"
/>
</van-popup>
<!-- 分类账单列表弹出层 -->
<van-popup
v-model:show="billListVisible"
position="bottom"
:style="{ height: '85%' }"
round
closeable
>
<div class="category-bills">
<div class="popup-header">
<h3>{{ selectedCategoryTitle }}</h3>
<p v-if="categoryBillsTotal"> {{ categoryBillsTotal }} 笔交易</p>
</div>
<div class="bills-scroll-container">
<TransactionList
:transactions="categoryBills"
:loading="billListLoading"
:finished="billListFinished"
:show-delete="false"
@load="loadCategoryBills"
@click="viewBillDetail"
/>
</div>
</div>
</van-popup>
<!-- 交易详情编辑组件 -->
<TransactionDetail
v-model:show="detailVisible"
:transaction="currentTransaction"
@save="onBillSave"
/>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onActivated } from 'vue'
import { showToast } from 'vant'
import { useRouter } from 'vue-router'
import { getMonthlyStatistics, getCategoryStatistics, getTrendStatistics } from '@/api/statistics'
import { getTransactionList, getTransactionDetail } from '@/api/transactionRecord'
import TransactionList from '@/components/TransactionList.vue'
import TransactionDetail from '@/components/TransactionDetail.vue'
const router = useRouter()
// 响应式数据
const loading = ref(true)
const refreshing = ref(false)
const showMonthPicker = ref(false)
const currentYear = ref(new Date().getFullYear())
const currentMonth = ref(new Date().getMonth() + 1)
const selectedDate = ref([new Date().getFullYear().toString(), (new Date().getMonth() + 1).toString().padStart(2, '0')])
// 账单列表相关
const billListVisible = ref(false)
const billListLoading = ref(false)
const billListFinished = ref(false)
const categoryBills = ref([])
const categoryBillsTotal = ref(0)
const selectedCategoryTitle = ref('')
const selectedClassify = ref('')
const selectedType = ref(null)
const billPageIndex = ref(1)
const billPageSize = 20
// 详情编辑相关
const detailVisible = ref(false)
const currentTransaction = ref(null)
// 月度数据
const monthlyData = ref({
totalExpense: 0,
totalIncome: 0,
balance: 0,
expenseCount: 0,
incomeCount: 0,
totalCount: 0,
maxExpense: 0,
maxIncome: 0
})
// 分类数据
const expenseCategories = ref([])
const incomeCategories = ref([])
const noneCategories = ref([])
// 趋势数据
const trendData = ref([])
// 日期范围
const minDate = new Date(2020, 0, 1)
const maxDate = new Date()
// 颜色配置
const colors = [
'#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A', '#98D8C8',
'#F7DC6F', '#BB8FCE', '#85C1E2', '#F8B88B', '#AAB7B8',
'#FF8ED4', '#67E6DC', '#FFAB73', '#C9B1FF', '#7BDFF2'
]
// 计算环形图数据
const circumference = computed(() => 2 * Math.PI * 70)
const chartSegments = computed(() => {
let offset = 0
return expenseCategories.value.map((category) => {
const percent = category.percent / 100
const length = circumference.value * percent
const segment = {
color: category.color,
length,
offset
}
offset += length
return segment
})
})
// 日均统计
const dailyAverage = computed(() => {
const daysInMonth = new Date(currentYear.value, currentMonth.value, 0).getDate()
return {
expense: monthlyData.value.totalExpense / daysInMonth,
income: monthlyData.value.totalIncome / daysInMonth
}
})
// 趋势图最大值
const maxTrendValue = computed(() => {
const allValues = trendData.value.flatMap(item => [item.expense, item.income])
return Math.max(...allValues, 1)
})
// 是否是当前月
const isCurrentMonth = computed(() => {
const now = new Date()
return currentYear.value === now.getFullYear() && currentMonth.value === now.getMonth() + 1
})
// 格式化金额
const formatMoney = (value) => {
if (!value && value !== 0) return '0.00'
return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}
// 格式化短金额k为单位
const formatShortMoney = (value) => {
if (!value) return '0'
if (value >= 10000) {
return (value / 10000).toFixed(1) + 'w'
}
if (value >= 1000) {
return (value / 1000).toFixed(1) + 'k'
}
return value.toFixed(0)
}
// 获取柱状图高度
const getBarHeight = (value, maxValue) => {
if (!value || !maxValue) return '0%'
const percent = (value / maxValue) * 100
return Math.max(percent, 5) + '%' // 最小5%以便显示
}
// 切换月份
const changeMonth = (offset) => {
let newMonth = currentMonth.value + offset
let newYear = currentYear.value
if (newMonth > 12) {
newMonth = 1
newYear++
} else if (newMonth < 1) {
newMonth = 12
newYear--
}
// 不能超过当前月份
const now = new Date()
const targetDate = new Date(newYear, newMonth - 1)
if (targetDate > now) {
return
}
currentYear.value = newYear
currentMonth.value = newMonth
fetchStatistics()
}
// 确认月份选择
const onMonthConfirm = ({ selectedValues }) => {
currentYear.value = parseInt(selectedValues[0])
currentMonth.value = parseInt(selectedValues[1])
showMonthPicker.value = false
fetchStatistics()
}
// 下拉刷新
const onRefresh = async () => {
await fetchStatistics()
refreshing.value = false
}
// 获取统计数据
const fetchStatistics = async () => {
loading.value = true
try {
await Promise.all([
fetchMonthlyData(),
fetchCategoryData(),
fetchTrendData()
])
} catch (error) {
console.error('获取统计数据失败:', error)
showToast('获取统计数据失败')
} finally {
loading.value = false
}
}
// 获取月度数据
const fetchMonthlyData = async () => {
try {
const response = await getMonthlyStatistics({
year: currentYear.value,
month: currentMonth.value
})
if (response.success && response.data) {
monthlyData.value = response.data
}
} catch (error) {
console.error('获取月度数据失败:', error)
showToast('获取月度数据失败')
}
}
// 获取分类数据
const fetchCategoryData = async () => {
try {
// 获取支出分类
const expenseResponse = await getCategoryStatistics({
year: currentYear.value,
month: currentMonth.value,
type: 0 // 支出
})
if (expenseResponse.success && expenseResponse.data) {
expenseCategories.value = expenseResponse.data.map((item, index) => ({
classify: item.classify,
amount: item.amount,
count: item.count,
percent: item.percent,
color: colors[index % colors.length]
}))
}
// 获取收入分类
const incomeResponse = await getCategoryStatistics({
year: currentYear.value,
month: currentMonth.value,
type: 1 // 收入
})
if (incomeResponse.success && incomeResponse.data) {
incomeCategories.value = incomeResponse.data.map(item => ({
classify: item.classify,
amount: item.amount,
count: item.count,
percent: item.percent
}))
}
// 获取不计收支分类
const noneResponse = await getCategoryStatistics({
year: currentYear.value,
month: currentMonth.value,
type: 2 // 不计收支
})
if (noneResponse.success && noneResponse.data) {
noneCategories.value = noneResponse.data.map(item => ({
classify: item.classify,
amount: item.amount,
count: item.count,
percent: item.percent
}))
}
} catch (error) {
console.error('获取分类数据失败:', error)
showToast('获取分类数据失败')
}
}
// 获取趋势数据
const fetchTrendData = async () => {
try {
// 计算开始年月当前月往前推5个月
let startYear = currentYear.value
let startMonth = currentMonth.value - 5
if (startMonth <= 0) {
startMonth += 12
startYear--
}
const response = await getTrendStatistics({
startYear,
startMonth,
monthCount: 6
})
if (response.success && response.data) {
trendData.value = response.data.map(item => ({
year: item.year,
month: item.month,
label: `${item.month}`,
expense: item.expense,
income: item.income
}))
}
} catch (error) {
console.error('获取趋势数据失败:', error)
showToast('获取趋势数据失败')
}
}
// 跳转到智能分析页面
const goToAnalysis = () => {
router.push('/bill-analysis')
}
// 打开分类账单列表
const goToCategoryBills = (classify, type) => {
selectedClassify.value = classify || '未分类'
selectedType.value = type
selectedCategoryTitle.value = `${classify || '未分类'} - ${type === 0 ? '支出' : '收入'}`
// 重置分页状态
categoryBills.value = []
categoryBillsTotal.value = 0
billPageIndex.value = 1
billListFinished.value = false
billListVisible.value = true
// 打开弹窗后加载数据
loadCategoryBills()
}
// 打开总支出/总收入的所有账单列表
const goToTypeOverviewBills = (type) => {
selectedClassify.value = null
selectedType.value = type
selectedCategoryTitle.value = `${type === 0 ? '总支出' : '总收入'} - 明细`
// 重置分页状态
categoryBills.value = []
billPageIndex.value = 1
billListFinished.value = false
billListVisible.value = true
// 打开弹窗后加载数据
loadCategoryBills()
}
// 加载分类账单数据
const loadCategoryBills = async () => {
if (billListLoading.value || billListFinished.value) return
billListLoading.value = true
try {
const params = {
pageIndex: billPageIndex.value,
pageSize: billPageSize,
type: selectedType.value,
year: currentYear.value,
month: currentMonth.value,
sortByAmount: true
}
// 仅当选择了分类时才添加classify参数
if (selectedClassify.value !== null) {
params.classify = selectedClassify.value
}
const response = await getTransactionList(params)
if (response.success) {
const newList = response.data || []
categoryBills.value = [...categoryBills.value, ...newList]
categoryBillsTotal.value = response.total
if (newList.length === 0 || newList.length < billPageSize) {
billListFinished.value = true
} else {
billListFinished.value = false
billPageIndex.value++
}
} else {
showToast(response.message || '加载账单失败')
billListFinished.value = true
}
} catch (error) {
console.error('加载分类账单失败:', error)
showToast('加载账单失败')
billListFinished.value = true
} finally {
billListLoading.value = false
}
}
// 查看账单详情
const viewBillDetail = async (transaction) => {
try {
const response = await getTransactionDetail(transaction.id)
if (response.success) {
currentTransaction.value = response.data
detailVisible.value = true
} else {
showToast(response.message || '获取详情失败')
}
} catch (error) {
console.error('获取详情出错:', error)
showToast('获取详情失败')
}
}
// 账单保存后的回调
const onBillSave = async () => {
// 刷新统计数据
await fetchStatistics()
// 刷新账单列表
categoryBills.value = []
billPageIndex.value = 1
billListFinished.value = false
await loadCategoryBills()
showToast('保存成功')
}
// 初始化
onMounted(() => {
fetchStatistics()
})
// 页面激活时刷新数据(从其他页面返回时)
onActivated(() => {
fetchStatistics()
})
</script>
<style scoped>
.statistics-content {
padding: 16px 0 0 0;
}
:deep(.van-pull-refresh) {
flex: 1;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
/* 月度概览卡片 */
.overview-card {
display: flex;
justify-content: space-around;
align-items: center;
background: #ffffff;
margin: 0 12px 16px;
padding: 24px 12px;
border-radius: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
border: 1px solid #ebedf0;
}
@media (prefers-color-scheme: dark) {
.overview-card {
background: #1f1f1f;
border-color: #2c2c2c;
}
}
.overview-item {
flex: 1;
text-align: center;
}
.overview-item.clickable {
cursor: pointer;
transition: background-color 0.2s;
padding: 8px;
border-radius: 8px;
}
.overview-item.clickable:active {
background-color: #f0f0f0;
}
@media (prefers-color-scheme: dark) {
.overview-item.clickable:active {
background-color: #2c2c2c;
}
}
.overview-item .label {
font-size: 13px;
color: var(--van-text-color-2);
margin-bottom: 8px;
}
.overview-item .value {
font-size: 20px;
font-weight: 700;
margin-bottom: 4px;
}
.overview-item .sub-text {
font-size: 12px;
color: var(--van-text-color-3);
}
.divider {
width: 1px;
height: 40px;
background: var(--van-border-color);
}
.expense {
color: #ff6b6b;
}
.income {
color: #51cf66;
}
/* 环形图 */
.chart-container {
padding: 20px;
}
.ring-chart {
position: relative;
width: 200px;
height: 200px;
margin: 0 auto;
}
.ring-svg {
width: 100%;
height: 100%;
transform: scale(1);
}
.ring-segment {
transition: all 0.3s ease;
}
.ring-center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
}
.center-value {
font-size: 20px;
font-weight: 700;
color: var(--van-text-color);
margin-bottom: 4px;
}
.center-label {
font-size: 13px;
color: var(--van-text-color-2);
}
/* 分类列表 */
.category-list {
padding: 0;
}
.category-item {
display: flex;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #ebedf0;
transition: background-color 0.2s;
gap: 12px;
}
@media (prefers-color-scheme: dark) {
.category-item {
border-bottom: 1px solid #2c2c2c;
}
}
.category-item:last-child {
border-bottom: none;
}
.category-item.clickable {
cursor: pointer;
}
.category-item.clickable:active {
background-color: #f7f8fa;
}
@media (prefers-color-scheme: dark) {
.category-item.clickable:active {
background-color: #141414;
}
}
.category-info {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
min-width: 0;
}
.category-name-with-count {
display: flex;
align-items: center;
gap: 8px;
}
.category-color {
width: 12px;
height: 12px;
border-radius: 50%;
}
.category-name {
font-size: 14px;
color: var(--van-text-color);
}
.category-count {
font-size: 12px;
color: var(--van-text-color-3);
}
.category-stats {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.category-arrow {
margin-left: 8px;
color: var(--van-text-color-3);
font-size: 16px;
flex-shrink: 0;
}
.category-amount {
font-size: 15px;
font-weight: 600;
color: var(--van-text-color);
}
.category-percent {
font-size: 12px;
color: var(--van-text-color-3);
background: #f7f8fa;
padding: 2px 8px;
border-radius: 10px;
}
@media (prefers-color-scheme: dark) {
.category-percent {
background: #141414;
}
}
.income-color {
background-color: #51cf66;
}
.income-text {
color: #51cf66;
}
/* 不计收支颜色 */
.none-color {
background-color: #909399;
}
.none-text {
color: #909399;
}
.expense-color {
background-color: #ff6b6b;
}
/* 趋势图 */
.trend-chart {
padding: 20px 16px;
}
.trend-bars {
display: flex;
justify-content: space-between;
align-items: flex-end;
height: 180px;
margin-bottom: 16px;
padding: 0 4px;
}
.trend-bar-group {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.bar-container {
width: 100%;
height: 150px;
display: flex;
justify-content: center;
align-items: flex-end;
gap: 4px;
padding: 0 2px;
}
.bar {
flex: 1;
max-width: 20px;
min-height: 4px;
border-radius: 4px 4px 0 0;
position: relative;
transition: all 0.3s ease;
display: flex;
align-items: flex-start;
justify-content: center;
}
.expense-bar {
background: linear-gradient(180deg, #ff6b6b 0%, #ff8787 100%);
}
.income-bar {
background: linear-gradient(180deg, #51cf66 0%, #69db7c 100%);
}
.bar-value {
font-size: 10px;
color: var(--van-text-color-2);
white-space: nowrap;
margin-top: -18px;
}
.bar-label {
font-size: 11px;
color: var(--van-text-color-3);
}
.trend-legend {
display: flex;
justify-content: center;
gap: 24px;
padding-top: 12px;
border-top: 1px solid #ebedf0;
}
@media (prefers-color-scheme: dark) {
.trend-legend {
border-top: 1px solid #2c2c2c;
}
}
.legend-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--van-text-color-2);
}
.legend-color {
width: 12px;
height: 12px;
border-radius: 2px;
}
/* 其他统计 */
.other-stats {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
padding: 16px;
}
.stat-item {
background: #f7f8fa;
padding: 16px;
border-radius: 12px;
text-align: center;
}
@media (prefers-color-scheme: dark) {
.stat-item {
background: #141414;
}
}
.stat-label {
font-size: 13px;
color: var(--van-text-color-2);
margin-bottom: 8px;
}
.stat-value {
font-size: 18px;
font-weight: 700;
color: var(--van-text-color);
}
/* 分类账单列表弹出层 */
.category-bills {
height: 100%;
display: flex;
flex-direction: column;
background: #f7f8fa;
overflow: hidden;
}
@media (prefers-color-scheme: dark) {
.category-bills {
background: #141414;
}
}
.category-bills .popup-header {
padding: 20px 16px;
background: #fff;
border-bottom: 1px solid #ebedf0;
flex-shrink: 0;
}
@media (prefers-color-scheme: dark) {
.category-bills .popup-header {
background: #1f1f1f;
border-bottom: 1px solid #2c2c2c;
}
}
.category-bills .popup-header h3 {
margin: 0 0 8px 0;
font-size: 18px;
font-weight: 600;
color: var(--van-text-color);
}
.category-bills .popup-header p {
margin: 0;
font-size: 14px;
color: var(--van-text-color-2);
}
/* 修复深色模式下van组件的背景色 */
.category-bills :deep(.van-list) {
background: transparent;
}
.category-bills :deep(.van-cell-group) {
background: transparent;
}
@media (prefers-color-scheme: dark) {
.category-bills :deep(.van-cell) {
background-color: #1f1f1f;
}
.category-bills :deep(.van-swipe-cell) {
background-color: transparent;
}
}
/* 设置页面容器背景色 */
:deep(.van-nav-bar) {
background: transparent !important;
}
</style>