Some checks failed
Docker Build & Deploy / Build Docker Image (push) Failing after 4m47s
Docker Build & Deploy / Deploy to Production (push) Has been skipped
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 2s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s
- TransactionDetail, CategoryBillPopup 移入 Transaction/ - BudgetTypeTabs 移入 Budget/ - GlassBottomNav, ModernEmpty 移入 Global/ - Icon, IconSelector, ClassifySelector 等 8 个通用组件移入 Common/ - 更新所有相关引用路径
816 lines
23 KiB
Vue
816 lines
23 KiB
Vue
<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>
|
||
|
||
<!-- 分类账单弹窗 -->
|
||
<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/Common/DateSelectHeader.vue'
|
||
import TimePeriodTabs from '@/components/Common/TimePeriodTabs.vue'
|
||
import MonthlyExpenseCard from './modules/MonthlyExpenseCard.vue'
|
||
import ExpenseCategoryCard from './modules/ExpenseCategoryCard.vue'
|
||
import IncomeNoneCategoryCard from './modules/IncomeNoneCategoryCard.vue'
|
||
import CategoryBillPopup from '@/components/Transaction/CategoryBillPopup.vue'
|
||
import {
|
||
// 新统一接口
|
||
getDailyStatisticsByRange,
|
||
getSummaryByRange,
|
||
getCategoryStatisticsByRange,
|
||
getTrendStatistics,
|
||
// 旧接口(兼容性保留)
|
||
getMonthlyStatistics,
|
||
getCategoryStatistics,
|
||
getDailyStatistics
|
||
} 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 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) {
|
||
// 转换数据格式:添加完整的 date 字段
|
||
trendStats.value = dailyResult.data
|
||
.filter((item) => item != null)
|
||
.map((item) => ({
|
||
date: `${year}-${month.toString().padStart(2, '0')}-${item.day.toString().padStart(2, '0')}`,
|
||
expense: item.expense || 0,
|
||
income: item.income || 0,
|
||
count: item.count || 0
|
||
}))
|
||
}
|
||
} 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() + 7) // 修改:+7 天,因为 endDate 是不包含的
|
||
|
||
const startDateStr = formatDateToString(weekStart)
|
||
const endDateStr = formatDateToString(weekEnd)
|
||
|
||
// 使用新的统一接口获取周统计汇总
|
||
const weekSummaryResult = await getSummaryByRange({
|
||
startDate: startDateStr,
|
||
endDate: endDateStr
|
||
})
|
||
|
||
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 getDailyStatisticsByRange({
|
||
startDate: startDateStr,
|
||
endDate: endDateStr
|
||
})
|
||
|
||
if (dailyResult?.success && dailyResult.data) {
|
||
// ⚠️ 注意: API 返回的 data 按日期顺序排列,但只有 day 字段(天数)
|
||
// 需要根据 weekStart 和索引重建完整日期
|
||
trendStats.value = dailyResult.data.map((item, index) => {
|
||
// 从 weekStart 开始,按索引递增天数
|
||
const date = new Date(weekStart)
|
||
date.setDate(weekStart.getDate() + index)
|
||
const dateStr = formatDateToString(date)
|
||
|
||
return {
|
||
date: dateStr,
|
||
expense: item.expense || 0,
|
||
income: item.income || 0,
|
||
count: item.count || 0
|
||
}
|
||
})
|
||
}
|
||
} catch (error) {
|
||
console.error('加载周度数据失败:', error)
|
||
}
|
||
}
|
||
|
||
// 加载分类统计
|
||
const loadCategoryStatistics = async (year, month) => {
|
||
try {
|
||
const categoryYear = year
|
||
// 如果是年度统计,month应该传0表示查询全年
|
||
const categoryMonth = currentPeriod.value === 'year' ? 0 : month
|
||
|
||
// 对于周统计,使用日期范围进行分类统计
|
||
if (currentPeriod.value === 'week') {
|
||
const weekStart = getWeekStartDate(currentDate.value)
|
||
const weekEnd = new Date(weekStart)
|
||
weekEnd.setDate(weekStart.getDate() + 7) // 修改:+7 天,因为 endDate 是不包含的
|
||
|
||
const startDateStr = formatDateToString(weekStart)
|
||
const endDateStr = formatDateToString(weekEnd)
|
||
|
||
// 使用新的统一接口并发加载支出、收入和不计收支分类
|
||
const [expenseResult, incomeResult, noneResult] = await Promise.allSettled([
|
||
getCategoryStatisticsByRange({ startDate: startDateStr, endDate: endDateStr, type: 0 }),
|
||
getCategoryStatisticsByRange({ startDate: startDateStr, endDate: endDateStr, type: 1 }),
|
||
getCategoryStatisticsByRange({ 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
|
||
// 重置 touchEnd 值,防止使用上次的残留值
|
||
touchEndX.value = touchStartX.value
|
||
touchEndY.value = touchStartY.value
|
||
}
|
||
|
||
const handleTouchMove = (e) => {
|
||
touchEndX.value = e.touches[0].clientX
|
||
touchEndY.value = e.touches[0].clientY
|
||
}
|
||
|
||
const handleTouchEnd = (e) => {
|
||
// 如果 touchEnd 事件中还有 changedTouches,使用它来获取最终位置
|
||
if (e.changedTouches && e.changedTouches.length > 0) {
|
||
touchEndX.value = e.changedTouches[0].clientX
|
||
touchEndY.value = e.changedTouches[0].clientY
|
||
}
|
||
|
||
const deltaX = touchEndX.value - touchStartX.value
|
||
const deltaY = touchEndY.value - touchStartY.value
|
||
|
||
// 最小滑动距离阈值(像素)
|
||
const MIN_SWIPE_DISTANCE = 50
|
||
|
||
// 判断是否是水平滑动(水平距离大于垂直距离且超过阈值)
|
||
if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > MIN_SWIPE_DISTANCE) {
|
||
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-primary);
|
||
/* 改善滚动性能 */
|
||
will-change: scroll-position;
|
||
/* 防止滚动卡顿 */
|
||
scroll-behavior: smooth;
|
||
}
|
||
|
||
.statistics-content {
|
||
padding: var(--spacing-md);
|
||
padding-bottom: calc(95px + 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(95px + env(safe-area-inset-bottom, 0px));
|
||
}
|
||
}
|
||
</style>
|