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/ - 更新所有相关引用路径
396 lines
11 KiB
Vue
396 lines
11 KiB
Vue
<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>
|