Files
EmailBill/Web/src/views/CalendarView.vue
SunCheng 0ef4b52fcc
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 39s
Docker Build & Deploy / Deploy to Production (push) Successful in 19s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s
fix
2026-01-17 15:03:19 +08:00

359 lines
9.8 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 calendar-container">
<van-calendar
title="日历"
:poppable="false"
:show-confirm="false"
:formatter="formatterCalendar"
:min-date="minDate"
:max-date="maxDate"
@month-show="onMonthShow"
@select="onDateSelect"
/>
<ContributionHeatmap ref="heatmapRef" />
<!-- 底部安全距离 -->
<div style="height: calc(60px + env(safe-area-inset-bottom, 0px))" />
<!-- 日期交易列表弹出层 -->
<PopupContainer
v-model="listVisible"
:title="selectedDateText"
:subtitle="getBalance(dateTransactions)"
height="75%"
>
<template #header-actions>
<SmartClassifyButton
ref="smartClassifyButtonRef"
:transactions="dateTransactions"
@save="onSmartClassifySave"
/>
</template>
<TransactionList
:transactions="dateTransactions"
:loading="listLoading"
:finished="true"
:show-delete="true"
@click="viewDetail"
@delete="handleDateTransactionDelete"
/>
</PopupContainer>
<!-- 交易详情组件 -->
<TransactionDetail
v-model:show="detailVisible"
:transaction="currentTransaction"
@save="onDetailSave"
/>
</div>
</template>
<script setup>
import { ref, onMounted, nextTick, onBeforeUnmount } from 'vue'
import { showToast } from 'vant'
import request from '@/api/request'
import { getTransactionDetail, getTransactionsByDate } from '@/api/transactionRecord'
import TransactionList from '@/components/TransactionList.vue'
import TransactionDetail from '@/components/TransactionDetail.vue'
import SmartClassifyButton from '@/components/SmartClassifyButton.vue'
import PopupContainer from '@/components/PopupContainer.vue'
import ContributionHeatmap from '@/components/ContributionHeatmap.vue'
const dailyStatistics = ref({})
const listVisible = ref(false)
const detailVisible = ref(false)
const dateTransactions = ref([])
const currentTransaction = ref(null)
const listLoading = ref(false)
const selectedDate = ref(null)
const selectedDateText = ref('')
const heatmapRef = ref(null)
// 设置日历可选范围例如过去2年到未来1年
const minDate = new Date(new Date().getFullYear() - 2, 0, 1) // 2年前的1月1日
const maxDate = new Date(new Date().getFullYear() + 1, 11, 31) // 明年12月31日
onMounted(async () => {
await nextTick()
setTimeout(() => {
// 计算页面高度滚动3/4高度以显示更多日期
const height = document.querySelector('.calendar-container').clientHeight * 0.43
document.querySelector('.van-calendar__body').scrollBy({
top: -height,
behavior: 'smooth'
})
}, 300)
})
// 获取日历统计数据
const fetchDailyStatistics = async (year, month) => {
try {
const response = await request.get('/TransactionRecord/GetDailyStatistics', {
params: { year, month }
})
if (response.success && response.data) {
// 将数组转换为对象key为日期
const statsMap = {}
response.data.forEach((item) => {
console.warn(item)
statsMap[item.date] = {
count: item.count,
amount: (item.income - item.expense).toFixed(1)
}
})
dailyStatistics.value = {
...dailyStatistics.value,
...statsMap
}
}
} catch (error) {
console.error('获取日历统计数据失败:', error)
}
}
const smartClassifyButtonRef = ref(null)
// 获取指定日期的交易列表
const fetchDateTransactions = async (date) => {
try {
listLoading.value = true
const dateStr = date
.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
})
.replace(/\//g, '-')
const response = await getTransactionsByDate(dateStr)
if (response.success && response.data) {
// 根据金额从大到小排序
dateTransactions.value = response.data.sort((a, b) => b.amount - a.amount)
// 重置智能分类按钮
smartClassifyButtonRef.value?.reset()
} else {
dateTransactions.value = []
showToast(response.message || '获取交易列表失败')
}
} catch (error) {
console.error('获取日期交易列表失败:', error)
dateTransactions.value = []
showToast('获取交易列表失败')
} finally {
listLoading.value = false
}
}
const getBalance = (transactions) => {
let balance = 0
transactions.forEach((tx) => {
if (tx.type === 1) {
balance += tx.amount
} else if (tx.type === 0) {
balance -= tx.amount
}
})
if (balance >= 0) {
return `结余收入 ${balance.toFixed(1)}`
} else {
return `结余支出 ${(-balance).toFixed(1)}`
}
}
// 当月份显示时触发
const onMonthShow = ({ date }) => {
const year = date.getFullYear()
const month = date.getMonth() + 1
fetchDailyStatistics(year, month)
}
// 日期选择事件
const onDateSelect = (date) => {
selectedDate.value = date
selectedDateText.value = formatSelectedDate(date)
fetchDateTransactions(date)
listVisible.value = true
}
// 格式化选中的日期
const formatSelectedDate = (date) => {
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long'
})
}
// 查看详情
const viewDetail = async (transaction) => {
try {
const response = await getTransactionDetail(transaction.id)
if (response.success) {
currentTransaction.value = response.data
detailVisible.value = true
} else {
showToast(response.message || '获取详情失败')
}
} catch (error) {
console.error('获取详情出错:', error)
showToast('获取详情失败')
}
}
// 详情保存后的回调
const onDetailSave = async (saveData) => {
const item = dateTransactions.value.find((tx) => tx.id === saveData.id)
if (!item) {
return
}
// 如果分类发生了变化 移除智能分类的内容,防止被智能分类覆盖
if (item.classify !== saveData.classify) {
// 通知智能分类按钮组件移除指定项
smartClassifyButtonRef.value?.removeClassifiedTransaction(saveData.id)
item.upsetedClassify = ''
}
// 更新当前日期交易列表中的数据
Object.assign(item, saveData)
// 重新加载当前月份的统计数据
const now = selectedDate.value || new Date()
fetchDailyStatistics(now.getFullYear(), now.getMonth() + 1)
}
// 处理删除事件:从当前日期交易列表中移除,并刷新当日和当月统计
const handleDateTransactionDelete = async (transactionId) => {
dateTransactions.value = dateTransactions.value.filter((t) => t.id !== transactionId)
// 刷新当前日期以及当月的统计数据
const now = selectedDate.value || new Date()
fetchDailyStatistics(now.getFullYear(), now.getMonth() + 1)
}
// 智能分类保存回调
const onSmartClassifySave = async () => {
// 保存完成后重新加载数据
if (selectedDate.value) {
await fetchDateTransactions(selectedDate.value)
}
// 重新加载统计数据
const now = selectedDate.value || new Date()
fetchDailyStatistics(now.getFullYear(), now.getMonth() + 1)
}
const formatterCalendar = (day) => {
const dayCopy = { ...day }
if (dayCopy.date.toDateString() === new Date().toDateString()) {
dayCopy.text = '今天'
}
// 格式化日期为 yyyy-MM-dd
const dateKey = dayCopy.date
.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
})
.replace(/\//g, '-')
const stats = dailyStatistics.value[dateKey]
if (stats) {
dayCopy.topInfo = `${stats.count}` // 展示消费笔数
dayCopy.bottomInfo = `${stats.amount}` // 展示消费金额
}
return dayCopy
}
// 初始加载当前月份数据
const now = new Date()
fetchDailyStatistics(now.getFullYear(), now.getMonth() + 1)
// 全局删除事件监听,确保日历页面数据一致
const onGlobalTransactionDeleted = () => {
if (selectedDate.value) {
fetchDateTransactions(selectedDate.value)
}
const now = selectedDate.value || new Date()
fetchDailyStatistics(now.getFullYear(), now.getMonth() + 1)
heatmapRef.value?.refresh()
}
window.addEventListener &&
window.addEventListener('transaction-deleted', onGlobalTransactionDeleted)
onBeforeUnmount(() => {
window.removeEventListener &&
window.removeEventListener('transaction-deleted', onGlobalTransactionDeleted)
})
// 当有交易被新增/修改/批量更新时刷新
const onGlobalTransactionsChanged = () => {
if (selectedDate.value) {
fetchDateTransactions(selectedDate.value)
}
const now = selectedDate.value || new Date()
fetchDailyStatistics(now.getFullYear(), now.getMonth() + 1)
heatmapRef.value?.refresh()
}
window.addEventListener &&
window.addEventListener('transactions-changed', onGlobalTransactionsChanged)
onBeforeUnmount(() => {
window.removeEventListener &&
window.removeEventListener('transactions-changed', onGlobalTransactionsChanged)
})
</script>
<style scoped>
:deep(.van-calendar__header-title){
display: none;
}
.van-calendar {
background: transparent !important;
}
.calendar-container {
/* 使用准确的视口高度减去 TabBar 高度50px和安全区域 */
display: flex;
flex-direction: column;
overflow: hidden;
margin: 0;
padding: 0;
background-color: var(--van-background);
}
.calendar-container :deep(.van-calendar) {
height: calc(auto + 40px) !important;
flex: 1;
overflow: auto;
margin: 0;
padding: 0;
}
/* 移除日历组件可能的底部 padding */
.calendar-container :deep(.van-calendar__body) {
padding-bottom: 0 !important;
}
.calendar-container :deep(.van-calendar__months) {
padding-bottom: 0 !important;
}
/* 设置页面容器背景色 */
:deep(.van-calendar__header-title) {
background: transparent !important;
}
/* Add margin to bottom of heatmap to separate from tabbar */
:deep(.heatmap-card) {
flex-shrink: 0; /* Prevent heatmap from shrinking */
}
</style>