1
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 54s
Docker Build & Deploy / Deploy to Production (push) Successful in 9s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 54s
Docker Build & Deploy / Deploy to Production (push) Successful in 9s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s
This commit is contained in:
380
Web/src/views/calendarV2/Index.vue
Normal file
380
Web/src/views/calendarV2/Index.vue
Normal file
@@ -0,0 +1,380 @@
|
||||
<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>
|
||||
</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'
|
||||
|
||||
// 定义组件名称(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
|
||||
}
|
||||
|
||||
// 点击交易卡片 - 跳转到详情页
|
||||
const onTransactionClick = (txn) => {
|
||||
router.push({
|
||||
path: '/transaction-detail',
|
||||
query: { id: txn.id }
|
||||
})
|
||||
}
|
||||
|
||||
// 点击通知按钮
|
||||
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 {
|
||||
background-color: var(--bg-primary);
|
||||
font-family: var(--font-primary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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>
|
||||
Reference in New Issue
Block a user