1
This commit is contained in:
766
Web/src/views/statisticsV2/Index.vue
Normal file
766
Web/src/views/statisticsV2/Index.vue
Normal file
@@ -0,0 +1,766 @@
|
||||
<template>
|
||||
<van-config-provider :theme="theme">
|
||||
<div class="page-container-flex statistics-v2-wrapper">
|
||||
<!-- 头部年月选择器 -->
|
||||
<CalendarHeader
|
||||
:type="currentPeriod"
|
||||
:current-date="currentDate"
|
||||
:show-notification="true"
|
||||
@prev="handlePrevPeriod"
|
||||
@next="handleNextPeriod"
|
||||
@jump="showDatePicker = true"
|
||||
@notification="goToStatisticsV1"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<!-- 时间段选择器 -->
|
||||
<TimePeriodTabs
|
||||
:active-tab="currentPeriod"
|
||||
@change="handlePeriodChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 可滚动内容区域 -->
|
||||
<div class="statistics-scroll-content">
|
||||
<!-- 下拉刷新 -->
|
||||
<van-pull-refresh
|
||||
v-model="refreshing"
|
||||
@refresh="onRefresh"
|
||||
>
|
||||
<!-- 加载状态 -->
|
||||
<van-loading
|
||||
v-if="loading"
|
||||
vertical
|
||||
style="padding: 100px 0"
|
||||
>
|
||||
加载统计数据中...
|
||||
</van-loading>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<div
|
||||
v-else-if="hasError"
|
||||
class="error-state"
|
||||
>
|
||||
<van-empty
|
||||
image="error"
|
||||
:description="errorMessage || '加载数据时出现错误'"
|
||||
>
|
||||
<van-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="retryLoad"
|
||||
>
|
||||
重试
|
||||
</van-button>
|
||||
</van-empty>
|
||||
</div>
|
||||
|
||||
<!-- 统计内容 -->
|
||||
<div
|
||||
v-else
|
||||
class="statistics-content"
|
||||
@touchstart="handleTouchStart"
|
||||
@touchmove="handleTouchMove"
|
||||
@touchend="handleTouchEnd"
|
||||
>
|
||||
<!-- 收支结余和趋势卡片(合并) -->
|
||||
<MonthlyExpenseCard
|
||||
:amount="monthlyStats.totalExpense"
|
||||
:income="monthlyStats.totalIncome"
|
||||
:balance="monthlyStats.balance"
|
||||
:trend-data="trendStats"
|
||||
:period="currentPeriod"
|
||||
:current-date="currentDate"
|
||||
/>
|
||||
|
||||
<!-- 支出分类卡片 -->
|
||||
<ExpenseCategoryCard
|
||||
:categories="expenseCategories"
|
||||
:total-expense="monthlyStats.totalExpense"
|
||||
:colors="categoryColors"
|
||||
@category-click="goToCategoryBills"
|
||||
/>
|
||||
|
||||
<!-- 收入和不计收支分类卡片 -->
|
||||
<IncomeNoneCategoryCard
|
||||
:income-categories="incomeCategories"
|
||||
:none-categories="noneCategories"
|
||||
:total-income="monthlyStats.totalIncome"
|
||||
@category-click="goToCategoryBills"
|
||||
/>
|
||||
</div>
|
||||
</van-pull-refresh>
|
||||
</div>
|
||||
|
||||
<!-- 日期选择器 -->
|
||||
<van-popup
|
||||
v-model:show="showDatePicker"
|
||||
position="bottom"
|
||||
:style="{ height: '50%' }"
|
||||
>
|
||||
<van-date-picker
|
||||
v-model="selectedDate"
|
||||
:type="datePickerType"
|
||||
:min-date="minDate"
|
||||
:max-date="maxDate"
|
||||
@confirm="onDateConfirm"
|
||||
@cancel="showDatePicker = false"
|
||||
/>
|
||||
</van-popup>
|
||||
|
||||
<!-- 液态玻璃底部导航栏 -->
|
||||
<GlassBottomNav
|
||||
v-model="activeTab"
|
||||
@tab-click="handleTabClick"
|
||||
/>
|
||||
|
||||
<!-- 分类账单弹窗 -->
|
||||
<CategoryBillPopup
|
||||
v-model="billPopupVisible"
|
||||
:classify="selectedClassify"
|
||||
:type="selectedType"
|
||||
:year="currentDate.getFullYear()"
|
||||
:month="currentPeriod === 'year' ? 0 : currentDate.getMonth() + 1"
|
||||
@refresh="loadStatistics"
|
||||
/>
|
||||
</div>
|
||||
</van-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import CalendarHeader from '@/components/DateSelectHeader.vue'
|
||||
import TimePeriodTabs from '@/components/TimePeriodTabs.vue'
|
||||
import MonthlyExpenseCard from './modules/MonthlyExpenseCard.vue'
|
||||
import ExpenseCategoryCard from './modules/ExpenseCategoryCard.vue'
|
||||
import IncomeNoneCategoryCard from './modules/IncomeNoneCategoryCard.vue'
|
||||
import CategoryBillPopup from '@/components/CategoryBillPopup.vue'
|
||||
import GlassBottomNav from '@/components/GlassBottomNav.vue'
|
||||
import { getMonthlyStatistics, getCategoryStatistics, getCategoryStatisticsByDateRange, getDailyStatistics, getTrendStatistics, getWeeklyStatistics, getRangeStatistics } from '@/api/statistics'
|
||||
import { useMessageStore } from '@/stores/message'
|
||||
import { getCssVar } from '@/utils/theme'
|
||||
|
||||
// 为组件缓存设置名称
|
||||
defineOptions({
|
||||
name: 'StatisticsV2View'
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const messageStore = useMessageStore()
|
||||
|
||||
// 主题
|
||||
const theme = computed(() => messageStore.isDarkMode ? 'dark' : 'light')
|
||||
|
||||
// 底部导航栏
|
||||
const activeTab = ref('statistics')
|
||||
const handleTabClick = (_item, _index) => {
|
||||
// 导航逻辑已在组件内部处理
|
||||
}
|
||||
|
||||
// 状态管理
|
||||
const loading = ref(false)
|
||||
const refreshing = ref(false)
|
||||
const showDatePicker = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const hasError = ref(false)
|
||||
|
||||
// 分类账单弹窗状态
|
||||
const billPopupVisible = ref(false)
|
||||
const selectedClassify = ref('')
|
||||
const selectedType = ref(0)
|
||||
|
||||
// 触摸滑动相关状态
|
||||
const touchStartX = ref(0)
|
||||
const touchStartY = ref(0)
|
||||
const touchEndX = ref(0)
|
||||
const touchEndY = ref(0)
|
||||
|
||||
// 时间段选择
|
||||
const currentPeriod = ref('month')
|
||||
const currentDate = ref(new Date())
|
||||
const selectedDate = ref([])
|
||||
const minDate = new Date(2020, 0, 1)
|
||||
const maxDate = new Date()
|
||||
|
||||
// 统计数据
|
||||
const monthlyStats = ref({
|
||||
totalExpense: 0,
|
||||
totalIncome: 0,
|
||||
balance: 0,
|
||||
expenseCount: 0,
|
||||
incomeCount: 0
|
||||
})
|
||||
|
||||
const trendStats = ref([])
|
||||
const expenseCategories = ref([])
|
||||
const incomeCategories = ref([])
|
||||
const noneCategories = ref([])
|
||||
|
||||
// 颜色配置
|
||||
const categoryColors = [
|
||||
'#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7',
|
||||
'#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E9'
|
||||
]
|
||||
|
||||
// 计算属性
|
||||
const datePickerType = computed(() => {
|
||||
switch (currentPeriod.value) {
|
||||
case 'week':
|
||||
case 'month':
|
||||
return 'year-month'
|
||||
case 'year':
|
||||
return 'year'
|
||||
default:
|
||||
return 'year-month'
|
||||
}
|
||||
})
|
||||
|
||||
// 获取周的开始日期(周一)
|
||||
const getWeekStartDate = (date) => {
|
||||
const target = new Date(date.valueOf())
|
||||
const dayNr = (date.getDay() + 6) % 7 // 周一为0,周日为6
|
||||
target.setDate(target.getDate() - dayNr)
|
||||
target.setHours(0, 0, 0, 0)
|
||||
return target
|
||||
}
|
||||
|
||||
// 格式化日期为字符串
|
||||
const formatDateToString = (date) => {
|
||||
const year = date.getFullYear()
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
||||
const day = date.getDate().toString().padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
|
||||
// 加载统计数据
|
||||
const loadStatistics = async () => {
|
||||
if (loading.value && !refreshing.value) {
|
||||
return // 防止重复加载
|
||||
}
|
||||
|
||||
loading.value = !refreshing.value
|
||||
|
||||
try {
|
||||
const year = currentDate.value.getFullYear()
|
||||
const month = currentDate.value.getMonth() + 1
|
||||
|
||||
// 重置数据
|
||||
monthlyStats.value = {
|
||||
totalExpense: 0,
|
||||
totalIncome: 0,
|
||||
balance: 0,
|
||||
expenseCount: 0,
|
||||
incomeCount: 0
|
||||
}
|
||||
trendStats.value = []
|
||||
expenseCategories.value = []
|
||||
incomeCategories.value = []
|
||||
noneCategories.value = []
|
||||
|
||||
// 根据时间段加载不同的数据
|
||||
if (currentPeriod.value === 'month') {
|
||||
await loadMonthlyData(year, month)
|
||||
} else if (currentPeriod.value === 'year') {
|
||||
await loadYearlyData(year)
|
||||
} else if (currentPeriod.value === 'week') {
|
||||
await loadWeeklyData()
|
||||
}
|
||||
|
||||
// 加载分类统计
|
||||
await loadCategoryStatistics(year, month)
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载统计数据失败:', error)
|
||||
hasError.value = true
|
||||
errorMessage.value = error.message || '网络连接异常,请检查网络后重试'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重试加载
|
||||
const retryLoad = () => {
|
||||
hasError.value = false
|
||||
errorMessage.value = ''
|
||||
loadStatistics()
|
||||
}
|
||||
|
||||
// 加载月度数据
|
||||
const loadMonthlyData = async (year, month) => {
|
||||
try {
|
||||
// 月度统计
|
||||
const monthlyResult = await getMonthlyStatistics({ year, month })
|
||||
if (monthlyResult?.success && monthlyResult.data) {
|
||||
monthlyStats.value = {
|
||||
totalExpense: monthlyResult.data.totalExpense || 0,
|
||||
totalIncome: monthlyResult.data.totalIncome || 0,
|
||||
balance: (monthlyResult.data.totalIncome || 0) - (monthlyResult.data.totalExpense || 0),
|
||||
expenseCount: monthlyResult.data.expenseCount || 0,
|
||||
incomeCount: monthlyResult.data.incomeCount || 0
|
||||
}
|
||||
}
|
||||
|
||||
// 加载每日统计
|
||||
const dailyResult = await getDailyStatistics({ year, month })
|
||||
if (dailyResult?.success && dailyResult.data) {
|
||||
trendStats.value = dailyResult.data.filter(item => item != null)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载月度数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载年度数据
|
||||
const loadYearlyData = async (year) => {
|
||||
try {
|
||||
// 年度统计 - 使用趋势接口获取12个月数据
|
||||
const trendResult = await getTrendStatistics({ startYear: year, startMonth: 1, monthCount: 12 })
|
||||
if (trendResult?.success && trendResult.data) {
|
||||
// 计算年度汇总
|
||||
const yearTotal = trendResult.data.reduce((acc, item) => {
|
||||
const expense = item.expense || 0
|
||||
const income = item.income || 0
|
||||
return {
|
||||
totalExpense: acc.totalExpense + expense,
|
||||
totalIncome: acc.totalIncome + income,
|
||||
balance: acc.balance + income - expense
|
||||
}
|
||||
}, { totalExpense: 0, totalIncome: 0, balance: 0 })
|
||||
|
||||
monthlyStats.value = {
|
||||
...yearTotal,
|
||||
expenseCount: 0,
|
||||
incomeCount: 0
|
||||
}
|
||||
|
||||
trendStats.value = trendResult.data.map(item => ({
|
||||
date: `${item.year}-${item.month.toString().padStart(2, '0')}-01`,
|
||||
amount: (item.income || 0) - (item.expense || 0),
|
||||
count: 1
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载年度数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载周度数据
|
||||
const loadWeeklyData = async () => {
|
||||
try {
|
||||
// 周统计 - 计算当前周的开始和结束日期
|
||||
const weekStart = getWeekStartDate(currentDate.value)
|
||||
const weekEnd = new Date(weekStart)
|
||||
weekEnd.setDate(weekStart.getDate() + 6)
|
||||
|
||||
// 获取周统计汇总
|
||||
const weekSummaryResult = await getRangeStatistics({
|
||||
startDate: formatDateToString(weekStart),
|
||||
endDate: formatDateToString(weekEnd)
|
||||
})
|
||||
|
||||
if (weekSummaryResult?.success && weekSummaryResult.data) {
|
||||
monthlyStats.value = {
|
||||
totalExpense: weekSummaryResult.data.totalExpense || 0,
|
||||
totalIncome: weekSummaryResult.data.totalIncome || 0,
|
||||
balance: (weekSummaryResult.data.totalIncome || 0) - (weekSummaryResult.data.totalExpense || 0),
|
||||
expenseCount: weekSummaryResult.data.expenseCount || 0,
|
||||
incomeCount: weekSummaryResult.data.incomeCount || 0
|
||||
}
|
||||
}
|
||||
|
||||
// 获取周内每日统计
|
||||
const dailyResult = await getWeeklyStatistics({
|
||||
startDate: formatDateToString(weekStart),
|
||||
endDate: formatDateToString(weekEnd)
|
||||
})
|
||||
|
||||
if (dailyResult?.success && dailyResult.data) {
|
||||
// 转换数据格式以适配图表组件
|
||||
trendStats.value = dailyResult.data.map(item => ({
|
||||
date: item.date,
|
||||
amount: (item.income || 0) - (item.expense || 0),
|
||||
count: item.count || 0
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载周度数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载分类统计
|
||||
const loadCategoryStatistics = async (year, month) => {
|
||||
try {
|
||||
const categoryYear = year
|
||||
const categoryMonth = month
|
||||
|
||||
// 对于周统计,使用日期范围进行分类统计
|
||||
if (currentPeriod.value === 'week') {
|
||||
const weekStart = getWeekStartDate(currentDate.value)
|
||||
const weekEnd = new Date(weekStart)
|
||||
weekEnd.setDate(weekStart.getDate() + 6)
|
||||
weekEnd.setHours(23, 59, 59, 999)
|
||||
|
||||
const startDateStr = formatDateToString(weekStart)
|
||||
const endDateStr = formatDateToString(weekEnd)
|
||||
|
||||
// 并发加载支出、收入和不计收支分类(使用日期范围)
|
||||
const [expenseResult, incomeResult, noneResult] = await Promise.allSettled([
|
||||
getCategoryStatisticsByDateRange({ startDate: startDateStr, endDate: endDateStr, type: 0 }),
|
||||
getCategoryStatisticsByDateRange({ startDate: startDateStr, endDate: endDateStr, type: 1 }),
|
||||
getCategoryStatisticsByDateRange({ startDate: startDateStr, endDate: endDateStr, type: 2 })
|
||||
])
|
||||
|
||||
// 获取图表颜色配置
|
||||
const getChartColors = () => [
|
||||
getCssVar('--chart-color-1'),
|
||||
getCssVar('--chart-color-2'),
|
||||
getCssVar('--chart-color-3'),
|
||||
getCssVar('--chart-color-4'),
|
||||
getCssVar('--chart-color-5'),
|
||||
getCssVar('--chart-color-6'),
|
||||
getCssVar('--chart-color-7'),
|
||||
getCssVar('--chart-color-8'),
|
||||
getCssVar('--chart-color-9'),
|
||||
getCssVar('--chart-color-10'),
|
||||
getCssVar('--chart-color-11'),
|
||||
getCssVar('--chart-color-12'),
|
||||
getCssVar('--chart-color-13'),
|
||||
getCssVar('--chart-color-14'),
|
||||
getCssVar('--chart-color-15')
|
||||
]
|
||||
|
||||
const currentColors = getChartColors()
|
||||
|
||||
// 处理支出分类结果
|
||||
if (expenseResult.status === 'fulfilled' && expenseResult.value?.success && expenseResult.value.data) {
|
||||
expenseCategories.value = expenseResult.value.data.map((item, index) => ({
|
||||
classify: item.classify,
|
||||
amount: item.amount || 0,
|
||||
count: item.count || 0,
|
||||
percent: item.percent || 0,
|
||||
color: currentColors[index % currentColors.length]
|
||||
}))
|
||||
}
|
||||
|
||||
// 处理收入分类结果
|
||||
if (incomeResult.status === 'fulfilled' && incomeResult.value?.success && incomeResult.value.data) {
|
||||
incomeCategories.value = incomeResult.value.data.map((item) => ({
|
||||
classify: item.classify,
|
||||
amount: item.amount || 0,
|
||||
count: item.count || 0,
|
||||
percent: item.percent || 0
|
||||
}))
|
||||
}
|
||||
|
||||
// 处理不计收支分类结果
|
||||
if (noneResult.status === 'fulfilled' && noneResult.value?.success && noneResult.value.data) {
|
||||
noneCategories.value = noneResult.value.data.map((item) => ({
|
||||
classify: item.classify,
|
||||
amount: item.amount || 0,
|
||||
count: item.count || 0,
|
||||
percent: item.percent || 0
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
// 对于月度和年度统计,使用年月进行分类统计
|
||||
// 并发加载支出、收入和不计收支分类
|
||||
const [expenseResult, incomeResult, noneResult] = await Promise.allSettled([
|
||||
getCategoryStatistics({ year: categoryYear, month: categoryMonth, type: 0 }),
|
||||
getCategoryStatistics({ year: categoryYear, month: categoryMonth, type: 1 }),
|
||||
getCategoryStatistics({ year: categoryYear, month: categoryMonth, type: 2 })
|
||||
])
|
||||
|
||||
// 获取图表颜色配置
|
||||
const getChartColors = () => [
|
||||
getCssVar('--chart-color-1'),
|
||||
getCssVar('--chart-color-2'),
|
||||
getCssVar('--chart-color-3'),
|
||||
getCssVar('--chart-color-4'),
|
||||
getCssVar('--chart-color-5'),
|
||||
getCssVar('--chart-color-6'),
|
||||
getCssVar('--chart-color-7'),
|
||||
getCssVar('--chart-color-8'),
|
||||
getCssVar('--chart-color-9'),
|
||||
getCssVar('--chart-color-10'),
|
||||
getCssVar('--chart-color-11'),
|
||||
getCssVar('--chart-color-12'),
|
||||
getCssVar('--chart-color-13'),
|
||||
getCssVar('--chart-color-14'),
|
||||
getCssVar('--chart-color-15')
|
||||
]
|
||||
|
||||
const currentColors = getChartColors()
|
||||
|
||||
// 处理支出分类结果
|
||||
if (expenseResult.status === 'fulfilled' && expenseResult.value?.success && expenseResult.value.data) {
|
||||
expenseCategories.value = expenseResult.value.data.map((item, index) => ({
|
||||
classify: item.classify,
|
||||
amount: item.amount || 0,
|
||||
count: item.count || 0,
|
||||
percent: item.percent || 0,
|
||||
color: currentColors[index % currentColors.length]
|
||||
}))
|
||||
}
|
||||
|
||||
// 处理收入分类结果
|
||||
if (incomeResult.status === 'fulfilled' && incomeResult.value?.success && incomeResult.value.data) {
|
||||
incomeCategories.value = incomeResult.value.data.map((item) => ({
|
||||
classify: item.classify,
|
||||
amount: item.amount || 0,
|
||||
count: item.count || 0,
|
||||
percent: item.percent || 0
|
||||
}))
|
||||
}
|
||||
|
||||
// 处理不计收支分类结果
|
||||
if (noneResult.status === 'fulfilled' && noneResult.value?.success && noneResult.value.data) {
|
||||
noneCategories.value = noneResult.value.data.map((item) => ({
|
||||
classify: item.classify,
|
||||
amount: item.amount || 0,
|
||||
count: item.count || 0,
|
||||
percent: item.percent || 0
|
||||
}))
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载分类统计失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理时间段切换
|
||||
const handlePeriodChange = (period) => {
|
||||
currentPeriod.value = period
|
||||
// 清除错误状态
|
||||
hasError.value = false
|
||||
errorMessage.value = ''
|
||||
loadStatistics()
|
||||
}
|
||||
|
||||
// 切换时间周期
|
||||
const handlePrevPeriod = () => {
|
||||
const newDate = new Date(currentDate.value)
|
||||
|
||||
switch (currentPeriod.value) {
|
||||
case 'week':
|
||||
newDate.setDate(newDate.getDate() - 7)
|
||||
break
|
||||
case 'month':
|
||||
newDate.setMonth(newDate.getMonth() - 1)
|
||||
break
|
||||
case 'year':
|
||||
newDate.setFullYear(newDate.getFullYear() - 1)
|
||||
break
|
||||
}
|
||||
|
||||
currentDate.value = newDate
|
||||
// 清除错误状态
|
||||
hasError.value = false
|
||||
errorMessage.value = ''
|
||||
loadStatistics()
|
||||
}
|
||||
|
||||
const handleNextPeriod = () => {
|
||||
// 检查是否已经是最后一个周期(当前周期)
|
||||
if (isLastPeriod()) {
|
||||
return
|
||||
}
|
||||
|
||||
const newDate = new Date(currentDate.value)
|
||||
|
||||
switch (currentPeriod.value) {
|
||||
case 'week':
|
||||
newDate.setDate(newDate.getDate() + 7)
|
||||
break
|
||||
case 'month':
|
||||
newDate.setMonth(newDate.getMonth() + 1)
|
||||
break
|
||||
case 'year':
|
||||
newDate.setFullYear(newDate.getFullYear() + 1)
|
||||
break
|
||||
}
|
||||
|
||||
currentDate.value = newDate
|
||||
// 清除错误状态
|
||||
hasError.value = false
|
||||
errorMessage.value = ''
|
||||
loadStatistics()
|
||||
}
|
||||
|
||||
// 判断是否是最后一个周期(不能再往后切换)
|
||||
const isLastPeriod = () => {
|
||||
const now = new Date()
|
||||
const current = new Date(currentDate.value)
|
||||
|
||||
switch (currentPeriod.value) {
|
||||
case 'week': {
|
||||
// 获取当前周的开始日期和当前时间所在周的开始日期
|
||||
const currentWeekStart = getWeekStartDate(current)
|
||||
const nowWeekStart = getWeekStartDate(now)
|
||||
return currentWeekStart >= nowWeekStart
|
||||
}
|
||||
case 'month': {
|
||||
// 比较年月
|
||||
return current.getFullYear() === now.getFullYear() &&
|
||||
current.getMonth() === now.getMonth()
|
||||
}
|
||||
case 'year': {
|
||||
// 比较年份
|
||||
return current.getFullYear() === now.getFullYear()
|
||||
}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 触摸事件处理
|
||||
const handleTouchStart = (e) => {
|
||||
touchStartX.value = e.touches[0].clientX
|
||||
touchStartY.value = e.touches[0].clientY
|
||||
}
|
||||
|
||||
const handleTouchMove = (e) => {
|
||||
touchEndX.value = e.touches[0].clientX
|
||||
touchEndY.value = e.touches[0].clientY
|
||||
}
|
||||
|
||||
const handleTouchEnd = () => {
|
||||
const deltaX = touchEndX.value - touchStartX.value
|
||||
const deltaY = touchEndY.value - touchStartY.value
|
||||
|
||||
// 判断是否是水平滑动(水平距离大于垂直距离)
|
||||
if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > 50) {
|
||||
if (deltaX > 0) {
|
||||
// 右滑 - 上一个周期
|
||||
handlePrevPeriod()
|
||||
} else {
|
||||
// 左滑 - 下一个周期
|
||||
handleNextPeriod()
|
||||
}
|
||||
}
|
||||
|
||||
// 重置触摸位置
|
||||
touchStartX.value = 0
|
||||
touchStartY.value = 0
|
||||
touchEndX.value = 0
|
||||
touchEndY.value = 0
|
||||
}
|
||||
|
||||
// 下拉刷新
|
||||
const onRefresh = async () => {
|
||||
// 清除错误状态
|
||||
hasError.value = false
|
||||
errorMessage.value = ''
|
||||
await loadStatistics()
|
||||
refreshing.value = false
|
||||
}
|
||||
|
||||
// 日期选择确认
|
||||
const onDateConfirm = ({ selectedValues }) => {
|
||||
if (currentPeriod.value === 'year') {
|
||||
const [year] = selectedValues
|
||||
currentDate.value = new Date(year, 0, 1)
|
||||
} else {
|
||||
const [year, month] = selectedValues
|
||||
currentDate.value = new Date(year, month - 1, 1)
|
||||
}
|
||||
showDatePicker.value = false
|
||||
// 清除错误状态
|
||||
hasError.value = false
|
||||
errorMessage.value = ''
|
||||
loadStatistics()
|
||||
}
|
||||
|
||||
// 跳转到分类账单
|
||||
const goToCategoryBills = (classify, type) => {
|
||||
selectedClassify.value = classify || ''
|
||||
selectedType.value = type
|
||||
billPopupVisible.value = true
|
||||
}
|
||||
|
||||
// 切换到统计V1页面
|
||||
const goToStatisticsV1 = () => {
|
||||
router.push({ name: 'statistics' })
|
||||
}
|
||||
|
||||
// 监听时间段变化,更新选中日期
|
||||
watch(currentPeriod, () => {
|
||||
if (currentPeriod.value === 'year') {
|
||||
selectedDate.value = [currentDate.value.getFullYear()]
|
||||
} else {
|
||||
selectedDate.value = [
|
||||
currentDate.value.getFullYear(),
|
||||
currentDate.value.getMonth() + 1
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
// 设置默认选中日期
|
||||
selectedDate.value = [
|
||||
currentDate.value.getFullYear(),
|
||||
currentDate.value.getMonth() + 1
|
||||
]
|
||||
loadStatistics()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/assets/theme.css';
|
||||
|
||||
/* ========== 页面容器 ========== */
|
||||
.statistics-v2-wrapper {
|
||||
font-family: var(--font-primary);
|
||||
color: var(--text-primary);
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.statistics-scroll-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior: contain;
|
||||
background-color: var(--bg-secondary);
|
||||
/* 改善滚动性能 */
|
||||
will-change: scroll-position;
|
||||
/* 防止滚动卡顿 */
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.statistics-content {
|
||||
padding: var(--spacing-md);
|
||||
padding-bottom: calc(80px + env(safe-area-inset-bottom, 0px));
|
||||
min-height: 100%;
|
||||
/* 确保内容足够高以便滚动 */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.error-state {
|
||||
padding: var(--spacing-3xl) var(--spacing-md);
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 在移动设备上优化滚动体验 */
|
||||
@media (max-width: 768px) {
|
||||
.statistics-scroll-content {
|
||||
/* iOS Safari 优化 */
|
||||
-webkit-overflow-scrolling: touch;
|
||||
/* 防止橡皮筋效果 */
|
||||
overscroll-behavior-y: contain;
|
||||
}
|
||||
|
||||
.statistics-content {
|
||||
padding: var(--spacing-sm);
|
||||
padding-bottom: calc(90px + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user