Files
EmailBill/Web/src/views/calendarV2/Index.vue

396 lines
11 KiB
Vue
Raw Normal View History

2026-02-03 17:56:32 +08:00
<template>
<div class="page-container-flex calendar-v2-wrapper">
<!-- 头部固定 -->
2026-02-09 19:25:51 +08:00
<CalendarHeader
type="month"
:current-date="currentDate"
@prev="changeMonth(-1)"
@next="changeMonth(1)"
@jump="onDateJump"
@notification="onNotificationClick"
/>
2026-02-03 17:56:32 +08:00
<!-- 可滚动内容区域 -->
<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"
/>
<!-- 统计模块 -->
2026-02-15 10:10:28 +08:00
<StatsModule :selected-date="selectedDate" />
2026-02-03 17:56:32 +08:00
<!-- 交易列表模块 -->
<TransactionListModule
:selected-date="selectedDate"
@transaction-click="onTransactionClick"
@smart-click="onSmartClick"
/>
<!-- 底部安全距离 -->
<div class="bottom-spacer" />
</van-pull-refresh>
</div>
2026-02-04 15:36:42 +08:00
<!-- 交易详情弹窗 -->
<TransactionDetailSheet
v-model:show="showTransactionDetail"
:transaction="currentTransaction"
@save="handleTransactionSave"
@delete="handleTransactionDelete"
/>
2026-02-09 19:25:51 +08:00
<!-- 日期选择器弹窗 -->
<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>
2026-02-03 17:56:32 +08:00
</div>
</template>
<script setup>
2026-02-09 19:25:51 +08:00
import { ref, onMounted, onBeforeUnmount, onActivated, onDeactivated } from 'vue'
2026-02-03 17:56:32 +08:00
import { useRouter } from 'vue-router'
import { showToast } from 'vant'
2026-02-09 19:25:51 +08:00
import CalendarHeader from '@/components/DateSelectHeader.vue'
2026-02-03 17:56:32 +08:00
import CalendarModule from './modules/Calendar.vue'
import StatsModule from './modules/Stats.vue'
import TransactionListModule from './modules/TransactionList.vue'
2026-02-04 15:36:42 +08:00
import TransactionDetailSheet from '@/components/Transaction/TransactionDetailSheet.vue'
import { getTransactionDetail } from '@/api/transactionRecord'
2026-02-03 17:56:32 +08:00
// 定义组件名称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)
2026-02-09 19:25:51 +08:00
// 日期选择器相关
const showDatePicker = ref(false)
const pickerDate = ref(new Date())
2026-02-03 17:56:32 +08:00
// 格式化日期为 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()
2026-02-15 10:10:28 +08:00
slideDirection.value =
clickedMonth > currentMonth || (clickedMonth === 0 && currentMonth === 11)
? 'slide-left'
: 'slide-right'
2026-02-03 17:56:32 +08:00
// 更新 key 触发过渡
calendarKey.value += 1
// 切换到点击日期所在的月份
currentDate.value = new Date(clickedDate.getFullYear(), clickedDate.getMonth(), 1)
}
// 选中点击的日期
selectedDate.value = clickedDate
}
2026-02-04 15:36:42 +08:00
// 交易详情弹窗相关
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()
2026-02-03 17:56:32 +08:00
}
// 点击通知按钮
const onNotificationClick = () => {
router.push('/message')
}
2026-02-09 19:25:51 +08:00
// 点击日期标题,打开日期选择器
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()
2026-02-15 10:10:28 +08:00
if (
newDate.getFullYear() > today.getFullYear() ||
(newDate.getFullYear() === today.getFullYear() && newDate.getMonth() > today.getMonth())
) {
2026-02-09 19:25:51 +08:00
showToast('不能选择未来的月份')
showDatePicker.value = false
return
}
// 更新日期
currentDate.value = newDate
2026-02-11 13:00:01 +08:00
// 判断是否选择了当前月(复用上面的 today 变量)
2026-02-15 10:10:28 +08:00
const isCurrentMonth =
newDate.getFullYear() === today.getFullYear() && newDate.getMonth() === today.getMonth()
2026-02-11 13:00:01 +08:00
// 如果选择的是当前月,选中今天;否则选中该月第一天
selectedDate.value = isCurrentMonth ? today : newDate
2026-02-09 19:25:51 +08:00
calendarKey.value += 1
showDatePicker.value = false
}
// 取消日期选择
const onDatePickerCancel = () => {
showDatePicker.value = false
}
2026-02-03 17:56:32 +08:00
// 点击 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
2026-02-11 13:00:01 +08:00
// 判断是否切换到当前月(复用上面的 today 变量)
2026-02-15 10:10:28 +08:00
const isCurrentMonth =
newDate.getFullYear() === today.getFullYear() && newDate.getMonth() === today.getMonth()
2026-02-11 13:00:01 +08:00
// 根据切换方向和是否为当前月选择合适的日期
2026-02-03 17:56:32 +08:00
let newSelectedDate
2026-02-11 13:00:01 +08:00
if (isCurrentMonth) {
// 如果切换到当前月,选中今天
newSelectedDate = today
} else if (offset > 0) {
2026-02-03 17:56:32 +08:00
// 切换到下个月,选中下个月的第一天
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;
2026-02-04 16:23:12 +08:00
background-color: var(--bg-primary);
2026-02-03 17:56:32 +08:00
}
2026-02-11 13:00:01 +08:00
/* 底部安全距离 - 匹配底部导航栏高度 */
2026-02-03 17:56:32 +08:00
.bottom-spacer {
2026-02-11 13:00:01 +08:00
height: calc(95px + env(safe-area-inset-bottom, 0px));
2026-02-03 17:56:32 +08:00
}
</style>