Files
EmailBill/Web/src/views/calendarV2/Index.vue
SunCheng 045158730f
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
refactor: 整理组件目录结构
- TransactionDetail, CategoryBillPopup 移入 Transaction/
- BudgetTypeTabs 移入 Budget/
- GlassBottomNav, ModernEmpty 移入 Global/
- Icon, IconSelector, ClassifySelector 等 8 个通用组件移入 Common/
- 更新所有相关引用路径
2026-02-21 10:10:16 +08:00

396 lines
11 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 calendar-v2-wrapper">
<!-- 头部固定 -->
<CalendarHeader
type="month"
:current-date="currentDate"
@prev="changeMonth(-1)"
@next="changeMonth(1)"
@jump="onDateJump"
@notification="onNotificationClick"
/>
<!-- 可滚动内容区域 -->
<div class="calendar-scroll-content">
<!-- 下拉刷新 -->
<van-pull-refresh
v-model="refreshing"
@refresh="onRefresh"
>
<!-- 日历模块 -->
<CalendarModule
:current-date="currentDate"
:selected-date="selectedDate"
:slide-direction="slideDirection"
:calendar-key="calendarKey"
@day-click="onDayClick"
@touch-start="onTouchStart"
@touch-move="onTouchMove"
@touch-end="onTouchEnd"
/>
<!-- 统计模块 -->
<StatsModule :selected-date="selectedDate" />
<!-- 交易列表模块 -->
<TransactionListModule
:selected-date="selectedDate"
@transaction-click="onTransactionClick"
@smart-click="onSmartClick"
/>
<!-- 底部安全距离 -->
<div class="bottom-spacer" />
</van-pull-refresh>
</div>
<!-- 交易详情弹窗 -->
<TransactionDetailSheet
v-model:show="showTransactionDetail"
:transaction="currentTransaction"
@save="handleTransactionSave"
@delete="handleTransactionDelete"
/>
<!-- 日期选择器弹窗 -->
<van-popup
v-model:show="showDatePicker"
position="bottom"
round
>
<van-date-picker
v-model="pickerDate"
title="选择年月"
:min-date="new Date(2020, 0, 1)"
:max-date="new Date()"
:columns-type="['year', 'month']"
@confirm="onDatePickerConfirm"
@cancel="onDatePickerCancel"
/>
</van-popup>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, onActivated, onDeactivated } from 'vue'
import { useRouter } from 'vue-router'
import { showToast } from 'vant'
import CalendarHeader from '@/components/Common/DateSelectHeader.vue'
import CalendarModule from './modules/Calendar.vue'
import StatsModule from './modules/Stats.vue'
import TransactionListModule from './modules/TransactionList.vue'
import TransactionDetailSheet from '@/components/Transaction/TransactionDetailSheet.vue'
import { getTransactionDetail } from '@/api/transactionRecord'
// 定义组件名称keep-alive 需要通过 name 识别)
defineOptions({
name: 'CalendarV2'
})
// 路由
const router = useRouter()
// 下拉刷新状态
const refreshing = ref(false)
// 当前日期
const currentDate = ref(new Date())
const selectedDate = ref(new Date())
// 动画方向和 key用于触发过渡
const slideDirection = ref('slide-left')
const calendarKey = ref(0)
// 日期选择器相关
const showDatePicker = ref(false)
const pickerDate = ref(new Date())
// 格式化日期为 key (yyyy-MM-dd)
const formatDateKey = (date) => {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
// 点击日期
const onDayClick = async (day) => {
const clickedDate = new Date(day.date)
// 如果点击的是其他月份的单元格,切换到对应的月份
if (!day.isCurrentMonth) {
// 设置动画方向:点击上月日期向右滑,点击下月日期向左滑
const clickedMonth = clickedDate.getMonth()
const currentMonth = currentDate.value.getMonth()
slideDirection.value =
clickedMonth > currentMonth || (clickedMonth === 0 && currentMonth === 11)
? 'slide-left'
: 'slide-right'
// 更新 key 触发过渡
calendarKey.value += 1
// 切换到点击日期所在的月份
currentDate.value = new Date(clickedDate.getFullYear(), clickedDate.getMonth(), 1)
}
// 选中点击的日期
selectedDate.value = clickedDate
}
// 交易详情弹窗相关
const showTransactionDetail = ref(false)
const currentTransaction = ref(null)
// 点击交易卡片 - 打开详情弹窗
const onTransactionClick = async (txn) => {
try {
// 获取完整的交易详情
const response = await getTransactionDetail(txn.id)
if (response.success && response.data) {
currentTransaction.value = response.data
showTransactionDetail.value = true
} else {
showToast('获取交易详情失败')
}
} catch (error) {
console.error('获取交易详情失败:', error)
showToast('获取交易详情失败')
}
}
// 保存交易后刷新列表
const handleTransactionSave = () => {
handleTransactionsChanged()
}
// 删除交易后刷新列表
const handleTransactionDelete = () => {
handleTransactionsChanged()
}
// 点击通知按钮
const onNotificationClick = () => {
router.push('/message')
}
// 点击日期标题,打开日期选择器
const onDateJump = () => {
pickerDate.value = new Date(currentDate.value)
showDatePicker.value = true
}
// 确认日期选择
const onDatePickerConfirm = ({ selectedValues }) => {
const [year, month] = selectedValues
const newDate = new Date(year, month - 1, 1)
// 检查是否超过当前月
const today = new Date()
if (
newDate.getFullYear() > today.getFullYear() ||
(newDate.getFullYear() === today.getFullYear() && newDate.getMonth() > today.getMonth())
) {
showToast('不能选择未来的月份')
showDatePicker.value = false
return
}
// 更新日期
currentDate.value = newDate
// 判断是否选择了当前月(复用上面的 today 变量)
const isCurrentMonth =
newDate.getFullYear() === today.getFullYear() && newDate.getMonth() === today.getMonth()
// 如果选择的是当前月,选中今天;否则选中该月第一天
selectedDate.value = isCurrentMonth ? today : newDate
calendarKey.value += 1
showDatePicker.value = false
}
// 取消日期选择
const onDatePickerCancel = () => {
showDatePicker.value = false
}
// 点击 Smart 按钮 - 跳转到智能分类页面
const onSmartClick = () => {
router.push({
path: '/smart-classification',
query: {
date: formatDateKey(selectedDate.value)
}
})
}
// 切换月份
const changeMonth = async (offset) => {
const newDate = new Date(currentDate.value)
newDate.setMonth(newDate.getMonth() + offset)
// 检查是否是最后一个月(当前月)且尝试切换到下一个月
const today = new Date()
const currentYear = currentDate.value.getFullYear()
const currentMonthValue = currentDate.value.getMonth()
const todayYear = today.getFullYear()
const todayMonth = today.getMonth()
// 如果当前显示的是今天所在的月份,且尝试切换到下一个月,则阻止
if (offset > 0 && currentYear === todayYear && currentMonthValue === todayMonth) {
showToast('已经是最后一个月了')
return
}
// 设置动画方向
slideDirection.value = offset > 0 ? 'slide-left' : 'slide-right'
// 更新 key 触发过渡
calendarKey.value += 1
currentDate.value = newDate
// 判断是否切换到当前月(复用上面的 today 变量)
const isCurrentMonth =
newDate.getFullYear() === today.getFullYear() && newDate.getMonth() === today.getMonth()
// 根据切换方向和是否为当前月选择合适的日期
let newSelectedDate
if (isCurrentMonth) {
// 如果切换到当前月,选中今天
newSelectedDate = today
} else if (offset > 0) {
// 切换到下个月,选中下个月的第一天
newSelectedDate = new Date(newDate.getFullYear(), newDate.getMonth(), 1)
} else {
// 切换到上一月,选中上一月的最后一天
newSelectedDate = new Date(newDate.getFullYear(), newDate.getMonth() + 1, 0)
}
selectedDate.value = newSelectedDate
}
// 触摸滑动相关
const touchStartX = ref(0)
const touchStartY = ref(0)
const touchEndX = ref(0)
const touchEndY = ref(0)
const isSwiping = ref(false)
const minSwipeDistance = 50 // 最小滑动距离(像素)
const onTouchStart = (e) => {
touchStartX.value = e.changedTouches[0].screenX
touchStartY.value = e.changedTouches[0].screenY
touchEndX.value = touchStartX.value
touchEndY.value = touchStartY.value
isSwiping.value = false
}
const onTouchMove = (e) => {
touchEndX.value = e.changedTouches[0].screenX
touchEndY.value = e.changedTouches[0].screenY
const deltaX = Math.abs(touchEndX.value - touchStartX.value)
const deltaY = Math.abs(touchEndY.value - touchStartY.value)
// 如果水平滑动距离大于垂直滑动距离,判定为滑动操作
if (deltaX > deltaY && deltaX > 10) {
isSwiping.value = true
// 阻止页面滚动
e.preventDefault()
}
}
const onTouchEnd = async () => {
const distance = touchStartX.value - touchEndX.value
const absDistance = Math.abs(distance)
// 只有在滑动状态下且达到最小滑动距离时才切换月份
if (isSwiping.value && absDistance > minSwipeDistance) {
if (distance > 0) {
// 向左滑动 - 下一月
await changeMonth(1)
} else {
// 向右滑动 - 上一月
await changeMonth(-1)
}
}
// 重置触摸位置
touchStartX.value = 0
touchStartY.value = 0
touchEndX.value = 0
touchEndY.value = 0
isSwiping.value = false
}
// 处理交易变更事件(来自全局添加账单)
const handleTransactionsChanged = () => {
// 触发子组件刷新:通过改变日期引用强制重新查询
const temp = selectedDate.value
selectedDate.value = new Date(temp)
}
// 下拉刷新
const onRefresh = async () => {
try {
// 触发子组件刷新
handleTransactionsChanged()
showToast({
message: '刷新成功',
duration: 1500
})
} catch (_error) {
showToast('刷新失败')
} finally {
refreshing.value = false
}
}
// 组件挂载
onMounted(async () => {
// 监听交易变更事件(来自全局添加账单)
window.addEventListener('transactions-changed', handleTransactionsChanged)
})
// 页面激活时的钩子(从缓存恢复时触发)
onActivated(() => {
// 依赖全局事件 'transactions-changed' 来刷新数据
})
// 页面失活时的钩子(被缓存时触发)
onDeactivated(() => {
// 目前 CalendarV2 没有需要清理的资源
})
// 组件卸载前清理
onBeforeUnmount(() => {
window.removeEventListener('transactions-changed', handleTransactionsChanged)
})
</script>
<style scoped>
@import '@/assets/theme.css';
/* ========== 页面容器 ========== */
.calendar-v2-wrapper {
font-family: var(--font-primary);
color: var(--text-primary);
}
.calendar-scroll-content {
flex: 1;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
overscroll-behavior: contain;
background-color: var(--bg-primary);
}
/* 底部安全距离 - 匹配底部导航栏高度 */
.bottom-spacer {
height: calc(95px + env(safe-area-inset-bottom, 0px));
}
</style>