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

416 lines
10 KiB
Vue
Raw Normal View History

2026-02-03 17:56:32 +08:00
<template>
<div class="page-container-flex calendar-v2-wrapper">
<!-- 头部固定 -->
<header class="calendar-header">
<button
class="month-nav-btn"
aria-label="上一月"
@click="changeMonth(-1)"
>
<van-icon name="arrow-left" />
</button>
<div class="header-content">
<h1 class="header-title">
{{ currentMonth }}
</h1>
</div>
<button
class="month-nav-btn"
aria-label="下一月"
@click="changeMonth(1)"
>
<van-icon name="arrow" />
</button>
<button
class="notif-btn"
aria-label="通知"
@click="onNotificationClick"
>
<van-icon name="bell" />
</button>
</header>
<!-- 可滚动内容区域 -->
<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>
2026-02-04 15:36:42 +08:00
<!-- 交易详情弹窗 -->
<TransactionDetailSheet
v-model:show="showTransactionDetail"
:transaction="currentTransaction"
@save="handleTransactionSave"
@delete="handleTransactionDelete"
/>
2026-02-03 17:56:32 +08:00
</div>
</template>
<script setup>
import { ref, computed, onMounted, onBeforeUnmount, onActivated, onDeactivated } from 'vue'
import { useRouter } from 'vue-router'
import { showToast } from 'vant'
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)
// 当前月份格式化(中文)
const currentMonth = computed(() => {
const year = currentDate.value.getFullYear()
const month = currentDate.value.getMonth() + 1
return `${year}${month}`
})
// 格式化日期为 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
}
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')
}
// 点击 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
// 根据切换方向选择合适的日期
let newSelectedDate
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);
2026-02-04 16:07:08 +08:00
background-color: var(--van-background);
2026-02-03 17:56:32 +08:00
}
.calendar-scroll-content {
flex: 1;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
overscroll-behavior: contain;
}
/* ========== 头部 ========== */
.calendar-header {
display: flex;
align-items: center;
justify-content: flex-start;
padding: 8px 24px;
gap: 8px;
background: transparent !important;
2026-02-04 16:07:08 +08:00
position: relative;
z-index: 1;
2026-02-03 17:56:32 +08:00
}
.header-content {
display: flex;
flex-direction: column;
gap: 4px;
}
.header-title {
font-family: var(--font-primary);
font-size: var(--font-2xl);
font-weight: var(--font-medium);
color: var(--text-primary);
margin: 0;
}
.notif-btn {
display: flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
border-radius: var(--radius-full);
background-color: var(--bg-button);
border: none;
cursor: pointer;
transition: opacity 0.2s;
margin-left: auto;
}
.notif-btn:active {
opacity: 0.7;
}
.month-nav-btn {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 18px;
background-color: transparent;
border: none;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s;
}
.month-nav-btn:active {
background-color: var(--bg-tertiary);
}
/* 底部安全距离 */
.bottom-spacer {
height: calc(60px + env(safe-area-inset-bottom, 0px));
}
</style>