This commit is contained in:
SunCheng
2026-02-15 10:10:28 +08:00
parent e51a3edd50
commit a88556c784
92 changed files with 6751 additions and 776 deletions

View File

@@ -1,4 +1,4 @@
<!-- eslint-disable vue/no-v-html -->
<!-- eslint-disable vue/no-v-html -->
<template>
<div class="page-container-flex">
<!-- 顶部导航栏 -->
@@ -94,11 +94,15 @@
</div>
<!-- 提示词设置弹窗 -->
<van-dialog
<PopupContainer
v-model:show="showPromptDialog"
title="编辑分析提示词"
:show-cancel-button="true"
show-cancel-button
show-confirm-button
confirm-text="保存"
cancel-text="取消"
@confirm="confirmPrompt"
@cancel="showPromptDialog = false"
>
<van-field
v-model="promptValue"
@@ -109,7 +113,7 @@
placeholder="输入自定义的分析提示词..."
show-word-limit
/>
</van-dialog>
</PopupContainer>
</div>
</template>
@@ -118,6 +122,7 @@ import { ref, nextTick } from 'vue'
import { useRouter } from 'vue-router'
import { showToast, showLoadingToast, closeToast } from 'vant'
import { getConfig, setConfig } from '@/api/config'
import PopupContainer from '@/components/PopupContainer.vue'
const router = useRouter()
const userInput = ref('')

View File

@@ -1,4 +1,4 @@
<template>
<template>
<div class="page-container-flex">
<van-nav-bar
:title="navTitle"
@@ -111,9 +111,13 @@
</div>
<!-- 新增分类对话框 -->
<van-dialog
<PopupContainer
v-model:show="showAddDialog"
title="新增分类"
show-cancel-button
show-confirm-button
confirm-text="确认"
cancel-text="取消"
@confirm="handleConfirmAdd"
@cancel="resetAddForm"
>
@@ -126,14 +130,18 @@
:rules="[{ required: true, message: '请输入分类名称' }]"
/>
</van-form>
</van-dialog>
</PopupContainer>
<!-- 编辑分类对话框 -->
<van-dialog
<PopupContainer
v-model:show="showEditDialog"
title="编辑分类"
show-cancel-button
show-confirm-button
confirm-text="保存"
cancel-text="取消"
@confirm="handleConfirmEdit"
@cancel="showEditDialog = false"
>
<van-form ref="editFormRef">
<van-field
@@ -144,22 +152,45 @@
:rules="[{ required: true, message: '请输入分类名称' }]"
/>
</van-form>
</van-dialog>
</PopupContainer>
<!-- 删除确认对话框 -->
<van-dialog
<PopupContainer
v-model:show="showDeleteConfirm"
title="删除分类"
message="删除后无法恢复,确定要删除吗?"
show-confirm-button
show-cancel-button
confirm-text="确定"
cancel-text="取消"
@confirm="handleConfirmDelete"
/>
@cancel="showDeleteConfirm = false"
>
<p style="text-align: center; padding: 20px; color: var(--van-text-color-2)">
删除后无法恢复确定要删除吗
</p>
</PopupContainer>
<!-- 删除图标确认对话框 -->
<PopupContainer
v-model:show="showDeleteIconConfirm"
title="删除图标"
show-confirm-button
show-cancel-button
confirm-text="确定"
cancel-text="取消"
@confirm="handleConfirmDeleteIcon"
@cancel="showDeleteIconConfirm = false"
>
<p style="text-align: center; padding: 20px; color: var(--van-text-color-2)">
确定要删除图标吗
</p>
</PopupContainer>
<!-- 图标选择对话框 -->
<van-dialog
<PopupContainer
v-model:show="showIconDialog"
title="选择图标"
show-cancel-button
@confirm="handleConfirmIconSelect"
:closeable="false"
>
<div class="icon-selector">
<div
@@ -185,7 +216,8 @@
>
<van-empty description="暂无图标" />
</div>
</div>
<template #footer>
<div class="icon-actions">
<van-button
type="primary"
@@ -196,9 +228,28 @@
>
{{ isGeneratingIcon ? 'AI生成中...' : '生成新图标' }}
</van-button>
<van-button
v-if="currentCategory && currentCategory.icon"
type="danger"
size="small"
plain
:disabled="isDeletingIcon"
style="margin-left: 20px;"
@click="handleDeleteIcon"
>
{{ isDeletingIcon ? '删除中...' : '删除图标' }}
</van-button>
<van-button
size="small"
plain
style="margin-left: 10px;"
@click="showIconDialog = false"
>
关闭
</van-button>
</div>
</div>
</van-dialog>
</template>
</PopupContainer>
</div>
</div>
</template>
@@ -213,8 +264,10 @@ import {
deleteCategory,
updateCategory,
generateIcon,
updateSelectedIcon
updateSelectedIcon,
deleteCategoryIcon
} from '@/api/transactionCategory'
import PopupContainer from '@/components/PopupContainer.vue'
const router = useRouter()
@@ -261,6 +314,10 @@ const currentCategory = ref(null) // 当前正在编辑图标的分类
const selectedIconIndex = ref(0)
const isGeneratingIcon = ref(false)
// 删除图标确认对话框
const showDeleteIconConfirm = ref(false)
const isDeletingIcon = ref(false)
// 计算导航栏标题
const navTitle = computed(() => {
if (currentLevel.value === 0) {
@@ -437,7 +494,9 @@ const handleGenerateIcon = async () => {
* 确认选择图标
*/
const handleConfirmIconSelect = async () => {
if (!currentCategory.value) {return}
if (!currentCategory.value) {
return
}
try {
showLoadingToast({
@@ -466,6 +525,51 @@ const handleConfirmIconSelect = async () => {
}
}
/**
* 删除图标
*/
const handleDeleteIcon = () => {
if (!currentCategory.value || !currentCategory.value.icon) {
return
}
showDeleteIconConfirm.value = true
}
/**
* 确认删除图标
*/
const handleConfirmDeleteIcon = async () => {
if (!currentCategory.value) {
return
}
try {
isDeletingIcon.value = true
showLoadingToast({
message: '删除中...',
forbidClick: true,
duration: 0
})
const { success, message } = await deleteCategoryIcon(currentCategory.value.id)
if (success) {
showSuccessToast('图标删除成功')
showDeleteIconConfirm.value = false
showIconDialog.value = false
await loadCategories()
} else {
showToast(message || '删除失败')
}
} catch (error) {
console.error('删除图标失败:', error)
showToast('删除图标失败: ' + (error.message || '未知错误'))
} finally {
isDeletingIcon.value = false
closeToast()
}
}
/**
* 编辑分类
*/
@@ -564,7 +668,9 @@ const resetAddForm = () => {
* 解析图标数组(第一个图标为当前选中的)
*/
const parseIcon = (iconJson) => {
if (!iconJson) {return ''}
if (!iconJson) {
return ''
}
try {
const icons = JSON.parse(iconJson)
return Array.isArray(icons) && icons.length > 0 ? icons[0] : ''
@@ -577,7 +683,9 @@ const parseIcon = (iconJson) => {
* 解析图标数组为完整数组
*/
const parseIconArray = (iconJson) => {
if (!iconJson) {return []}
if (!iconJson) {
return []
}
try {
const icons = JSON.parse(iconJson)
return Array.isArray(icons) ? icons : []
@@ -679,12 +787,14 @@ onMounted(() => {
}
.icon-actions {
padding-top: 16px;
border-top: 1px solid var(--van-border-color);
display: flex;
justify-content: center;
gap: 8px;
padding: 8px 0;
}
/* PopupContainer 的 footer 已有边框,所以这里不需要重复 */
/* 深色模式 */
/* @media (prefers-color-scheme: dark) {
.level-container {

View File

@@ -120,7 +120,12 @@
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { showToast } from 'vant'
import { getLogList, getAvailableDates, getAvailableClassNames, getLogsByRequestId } from '@/api/log'
import {
getLogList,
getAvailableDates,
getAvailableClassNames,
getLogsByRequestId
} from '@/api/log'
const router = useRouter()

View File

@@ -507,7 +507,8 @@ const editPeriodic = (item) => {
form.type = parseInt(item.type)
form.classify = item.classify
form.periodicType = parseInt(item.periodicType)
form.periodicTypeText = periodicTypeColumns.find((t) => t.value === parseInt(item.periodicType))?.text || ''
form.periodicTypeText =
periodicTypeColumns.find((t) => t.value === parseInt(item.periodicType))?.text || ''
// 解析周期配置
if (item.periodicConfig) {

View File

@@ -16,7 +16,9 @@
<template #right>
<!-- 未覆盖分类警告图标支出和收入 tab -->
<van-icon
v-if="activeTab !== BudgetCategory.Savings && uncoveredCategories.length > 0 && !isArchive"
v-if="
activeTab !== BudgetCategory.Savings && uncoveredCategories.length > 0 && !isArchive
"
name="warning-o"
size="20"
color="var(--van-danger-color)"
@@ -285,7 +287,13 @@
<!-- 空状态 -->
<van-empty
v-if="activeTab !== BudgetCategory.Savings && !loading && !hasError && ((activeTab === BudgetCategory.Expense && expenseBudgets?.length === 0) || (activeTab === BudgetCategory.Income && incomeBudgets?.length === 0))"
v-if="
activeTab !== BudgetCategory.Savings &&
!loading &&
!hasError &&
((activeTab === BudgetCategory.Expense && expenseBudgets?.length === 0) ||
(activeTab === BudgetCategory.Income && incomeBudgets?.length === 0))
"
:description="`暂无${activeTab === BudgetCategory.Expense ? '支出' : '收入'}预算`"
/>
</div>
@@ -347,7 +355,10 @@
<div style="padding: 16px">
<div
class="rich-html-content"
v-html="archiveSummary || '<p style=\'text-align:center;color:var(--van-text-color-3)\'>暂无总结</p>'"
v-html="
archiveSummary ||
'<p style=\'text-align:center;color:var(--van-text-color-3)\'>暂无总结</p>'
"
/>
</div>
</PopupContainer>
@@ -402,7 +413,7 @@ defineOptions({
})
const messageStore = useMessageStore()
const theme = computed(() => messageStore.isDarkMode ? 'dark' : 'light')
const theme = computed(() => (messageStore.isDarkMode ? 'dark' : 'light'))
// 日期状态
const currentDate = ref(new Date())
@@ -607,11 +618,7 @@ const loadBudgetData = async () => {
try {
// 并发加载多个数据源
await Promise.allSettled([
loadMonthlyData(),
loadCategoryStats(),
loadUncoveredCategories()
])
await Promise.allSettled([loadMonthlyData(), loadCategoryStats(), loadUncoveredCategories()])
} catch (_error) {
console.error('加载预算数据失败:', _error)
hasError.value = true
@@ -910,7 +917,8 @@ onBeforeUnmount(() => {
}
.budget-content {
padding: 12px;
padding: var(--spacing-md);
padding-top: 0;
}
.error-state {
@@ -970,7 +978,9 @@ onBeforeUnmount(() => {
.item-amount {
font-size: 18px;
font-weight: 600;
font-family: DIN Alternate, system-ui;
font-family:
DIN Alternate,
system-ui;
}
.info-item {
@@ -988,7 +998,9 @@ onBeforeUnmount(() => {
.info-item .value {
font-size: 16px;
font-weight: 600;
font-family: DIN Alternate, system-ui;
font-family:
DIN Alternate,
system-ui;
}
.info-item .value.expense {

View File

@@ -71,125 +71,112 @@
</div>
<!-- 计划存款明细弹窗 -->
<van-popup
v-model:show="showDetailPopup"
position="bottom"
round
:style="{ height: '80%' }"
<PopupContainer
v-model="showDetailPopup"
title="计划存款明细"
height="80%"
>
<div class="detail-popup-content">
<div class="popup-header">
<h3 class="popup-title">
计划存款明细
</h3>
<van-icon
name="cross"
size="20"
class="close-icon"
@click="showDetailPopup = false"
/>
</div>
<div class="popup-body">
<div
v-if="currentBudget"
class="detail-content"
>
<div class="detail-section income-section">
<div class="section-title">
<van-icon name="balance-o" />
收入预算
<div class="popup-body">
<div
v-if="currentBudget"
class="detail-content"
>
<div class="detail-section income-section">
<div class="section-title">
<van-icon name="balance-o" />
收入预算
</div>
<div class="section-content">
<div class="detail-row">
<span class="detail-label">预算限额</span>
<span class="detail-value income">¥{{ formatMoney(incomeLimit) }}</span>
</div>
<div class="section-content">
<div class="detail-row">
<span class="detail-label">预算限额</span>
<span class="detail-value income">¥{{ formatMoney(incomeLimit) }}</span>
</div>
<div class="detail-row">
<span class="detail-label">实际收入</span>
<span class="detail-value">¥{{ formatMoney(incomeCurrent) }}</span>
</div>
<div class="detail-row">
<span class="detail-label">实际收入</span>
<span class="detail-value">¥{{ formatMoney(incomeCurrent) }}</span>
</div>
</div>
</div>
<div class="detail-section expense-section">
<div class="section-title">
<van-icon name="bill-o" />
支出预算
<div class="detail-section expense-section">
<div class="section-title">
<van-icon name="bill-o" />
支出预算
</div>
<div class="section-content">
<div class="detail-row">
<span class="detail-label">预算限额</span>
<span class="detail-value expense">¥{{ formatMoney(expenseLimit) }}</span>
</div>
<div class="section-content">
<div class="detail-row">
<span class="detail-label">预算限额</span>
<span class="detail-value expense">¥{{ formatMoney(expenseLimit) }}</span>
</div>
<div class="detail-row">
<span class="detail-label">实际支出</span>
<span class="detail-value">¥{{ formatMoney(expenseCurrent) }}</span>
</div>
<div class="detail-row">
<span class="detail-label">实际支出</span>
<span class="detail-value">¥{{ formatMoney(expenseCurrent) }}</span>
</div>
</div>
</div>
<div class="detail-section formula-section">
<div class="section-title">
<van-icon name="calculator-o" />
计划存款公式
<div class="detail-section formula-section">
<div class="section-title">
<van-icon name="calculator-o" />
计划存款公式
</div>
<div class="formula-box">
<div class="formula-item">
<span class="formula-label">收入预算</span>
<span class="formula-value income">¥{{ formatMoney(incomeLimit) }}</span>
</div>
<div class="formula-box">
<div class="formula-item">
<span class="formula-label">收入预算</span>
<span class="formula-value income">¥{{ formatMoney(incomeLimit) }}</span>
</div>
<div class="formula-operator">
-
</div>
<div class="formula-item">
<span class="formula-label">支出预算</span>
<span class="formula-value expense">¥{{ formatMoney(expenseLimit) }}</span>
</div>
<div class="formula-operator">
=
</div>
<div class="formula-item">
<span class="formula-label">计划存款</span>
<span class="formula-value">¥{{ formatMoney(currentBudget.limit) }}</span>
</div>
<div class="formula-operator">
-
</div>
<div class="formula-item">
<span class="formula-label">支出预算</span>
<span class="formula-value expense">¥{{ formatMoney(expenseLimit) }}</span>
</div>
<div class="formula-operator">
=
</div>
<div class="formula-item">
<span class="formula-label">计划存款</span>
<span class="formula-value">¥{{ formatMoney(currentBudget.limit) }}</span>
</div>
</div>
</div>
<div class="detail-section result-section">
<div class="section-title">
<van-icon name="chart-trending-o" />
存款结果
<div class="detail-section result-section">
<div class="section-title">
<van-icon name="chart-trending-o" />
存款结果
</div>
<div class="section-content">
<div class="detail-row">
<span class="detail-label">计划存款</span>
<span class="detail-value">¥{{ formatMoney(currentBudget.limit) }}</span>
</div>
<div class="section-content">
<div class="detail-row">
<span class="detail-label">计划存款</span>
<span class="detail-value">¥{{ formatMoney(currentBudget.limit) }}</span>
</div>
<div class="detail-row">
<span class="detail-label">实际存款</span>
<span
class="detail-value"
:class="{ income: currentBudget.current >= currentBudget.limit }"
>¥{{ formatMoney(currentBudget.current) }}</span>
</div>
<div class="detail-row highlight">
<span class="detail-label">还差</span>
<span class="detail-value expense">¥{{
formatMoney(Math.max(0, currentBudget.limit - currentBudget.current))
}}</span>
</div>
<div class="detail-row">
<span class="detail-label">实际存款</span>
<span
class="detail-value"
:class="{ income: currentBudget.current >= currentBudget.limit }"
>¥{{ formatMoney(currentBudget.current) }}</span>
</div>
<div class="detail-row highlight">
<span class="detail-label">还差</span>
<span class="detail-value expense">¥{{
formatMoney(Math.max(0, currentBudget.limit - currentBudget.current))
}}</span>
</div>
</div>
</div>
</div>
</div>
</van-popup>
</PopupContainer>
</template>
<script setup>
import { ref, computed } from 'vue'
import BudgetCard from '@/components/Budget/BudgetCard.vue'
import { BudgetPeriodType } from '@/constants/enums'
import PopupContainer from '@/components/PopupContainer.vue'
// Props
const props = defineProps({
@@ -349,41 +336,6 @@ const getProgressColor = (budget) => {
color: var(--van-success-color);
}
.detail-popup-content {
height: 100%;
display: flex;
flex-direction: column;
background-color: var(--van-background-2);
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid var(--van-border-color);
background-color: var(--van-background-2);
}
.popup-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: var(--van-text-color);
}
.close-icon {
color: var(--van-text-color-2);
cursor: pointer;
padding: 8px;
}
.popup-body {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.detail-content {
display: flex;
flex-direction: column;

View File

@@ -30,9 +30,7 @@
/>
<!-- 统计模块 -->
<StatsModule
:selected-date="selectedDate"
/>
<StatsModule :selected-date="selectedDate" />
<!-- 交易列表模块 -->
<TransactionListModule
@@ -125,9 +123,10 @@ const onDayClick = async (day) => {
const clickedMonth = clickedDate.getMonth()
const currentMonth = currentDate.value.getMonth()
slideDirection.value = clickedMonth > currentMonth || (clickedMonth === 0 && currentMonth === 11)
? 'slide-left'
: 'slide-right'
slideDirection.value =
clickedMonth > currentMonth || (clickedMonth === 0 && currentMonth === 11)
? 'slide-left'
: 'slide-right'
// 更新 key 触发过渡
calendarKey.value += 1
@@ -189,8 +188,10 @@ const onDatePickerConfirm = ({ selectedValues }) => {
// 检查是否超过当前月
const today = new Date()
if (newDate.getFullYear() > today.getFullYear() ||
(newDate.getFullYear() === today.getFullYear() && newDate.getMonth() > today.getMonth())) {
if (
newDate.getFullYear() > today.getFullYear() ||
(newDate.getFullYear() === today.getFullYear() && newDate.getMonth() > today.getMonth())
) {
showToast('不能选择未来的月份')
showDatePicker.value = false
return
@@ -200,8 +201,8 @@ const onDatePickerConfirm = ({ selectedValues }) => {
currentDate.value = newDate
// 判断是否选择了当前月(复用上面的 today 变量)
const isCurrentMonth = newDate.getFullYear() === today.getFullYear() &&
newDate.getMonth() === today.getMonth()
const isCurrentMonth =
newDate.getFullYear() === today.getFullYear() && newDate.getMonth() === today.getMonth()
// 如果选择的是当前月,选中今天;否则选中该月第一天
selectedDate.value = isCurrentMonth ? today : newDate
@@ -252,8 +253,8 @@ const changeMonth = async (offset) => {
currentDate.value = newDate
// 判断是否切换到当前月(复用上面的 today 变量)
const isCurrentMonth = newDate.getFullYear() === today.getFullYear() &&
newDate.getMonth() === today.getMonth()
const isCurrentMonth =
newDate.getFullYear() === today.getFullYear() && newDate.getMonth() === today.getMonth()
// 根据切换方向和是否为当前月选择合适的日期
let newSelectedDate

View File

@@ -133,7 +133,7 @@ const fetchDailyStats = async (year, month) => {
if (response.success && response.data) {
// 构建日期 Map
const statsMap = {}
response.data.forEach(item => {
response.data.forEach((item) => {
// 后端返回的是 day (1-31),需要构建完整的日期字符串
const dateKey = `${year}-${String(month).padStart(2, '0')}-${String(item.day).padStart(2, '0')}`
statsMap[dateKey] = {
@@ -160,7 +160,9 @@ const fetchAllRelevantMonthsData = async (year, month) => {
const firstDay = new Date(year, month, 1)
// 获取第一天是星期几 (0=Sunday, 调整为 0=Monday)
let startDayOfWeek = firstDay.getDay() - 1
if (startDayOfWeek === -1) {startDayOfWeek = 6}
if (startDayOfWeek === -1) {
startDayOfWeek = 6
}
// 判断是否需要加载上月数据
const needPrevMonth = startDayOfWeek > 0
@@ -173,7 +175,7 @@ const fetchAllRelevantMonthsData = async (year, month) => {
const totalCells = totalWeeks * 7
// 判断是否需要加载下月数据
const needNextMonth = totalCells > (startDayOfWeek + lastDay.getDate())
const needNextMonth = totalCells > startDayOfWeek + lastDay.getDate()
// 并行加载所有需要的月份数据
// JavaScript Date.month 是 0-11但后端 API 期望 1-12
@@ -221,7 +223,7 @@ const fetchHolidays = async (year, month) => {
const response = await getMonthHolidays(year, month)
if (response.success && response.data) {
const map = {}
response.data.forEach(item => {
response.data.forEach((item) => {
map[item.date] = item
})
holidaysMap.value = { ...holidaysMap.value, ...map }
@@ -233,11 +235,15 @@ const fetchHolidays = async (year, month) => {
}
// 监听 currentDate 变化,重新加载数据
watch(() => props.currentDate, async (newDate) => {
if (newDate) {
await fetchAllRelevantMonthsData(newDate.getFullYear(), newDate.getMonth())
}
}, { immediate: true })
watch(
() => props.currentDate,
async (newDate) => {
if (newDate) {
await fetchAllRelevantMonthsData(newDate.getFullYear(), newDate.getMonth())
}
},
{ immediate: true }
)
// 初始加载预算数据
fetchBudgetData()
@@ -254,7 +260,9 @@ const calendarWeeks = computed(() => {
// 获取第一天是星期几 (0=Sunday, 调整为 0=Monday)
let startDayOfWeek = firstDay.getDay() - 1
if (startDayOfWeek === -1) {startDayOfWeek = 6}
if (startDayOfWeek === -1) {
startDayOfWeek = 6
}
const weeks = []
let currentWeek = []
@@ -389,7 +397,9 @@ const onDayClick = (day) => {
// 节假日长按事件处理
const onTouchStartHoliday = (e, day) => {
if (!day.isHoliday) {return}
if (!day.isHoliday) {
return
}
// 长按500ms显示提示
holdTimer = setTimeout(() => {
@@ -558,22 +568,22 @@ const onTouchEnd = () => {
/* ========== 节假日样式 ========== */
/* 节假日放假样式(绿色系) */
.day-number.day-holiday {
background-color: #E8F5E9;
color: #2E7D32;
background-color: #e8f5e9;
color: #2e7d32;
font-weight: var(--font-bold);
}
/* 调休工作日样式(橙色/黄色系) */
.day-number.day-workday {
background-color: #FFF3E0;
color: #E65100;
background-color: #fff3e0;
color: #e65100;
font-weight: var(--font-bold);
}
/* 选中状态优先级最高 */
.day-number.day-selected {
background-color: var(--accent-primary) !important;
color: #FFFFFF !important;
color: #ffffff !important;
font-weight: var(--font-bold);
}

View File

@@ -63,11 +63,11 @@ const fetchDayStats = async (date) => {
if (response.success && response.data) {
// 计算当日支出和收入
selectedDayExpense.value = response.data
.filter(t => t.type === 0) // 只统计支出
.filter((t) => t.type === 0) // 只统计支出
.reduce((sum, t) => sum + t.amount, 0)
selectedDayIncome.value = response.data
.filter(t => t.type === 1) // 只统计收入
.filter((t) => t.type === 1) // 只统计收入
.reduce((sum, t) => sum + t.amount, 0)
}
} catch (error) {
@@ -78,11 +78,15 @@ const fetchDayStats = async (date) => {
}
// 监听 selectedDate 变化,重新加载数据
watch(() => props.selectedDate, async (newDate) => {
if (newDate) {
await fetchDayStats(newDate)
}
}, { immediate: true })
watch(
() => props.selectedDate,
async (newDate) => {
if (newDate) {
await fetchDayStats(newDate)
}
},
{ immediate: true }
)
// 判断是否为今天
const isToday = computed(() => {
@@ -112,7 +116,7 @@ const selectedDateFormatted = computed(() => {
flex-direction: column;
gap: var(--spacing-xl);
padding: var(--spacing-3xl);
padding-top: 8px
padding-top: 8px;
}
.stats-header {

View File

@@ -153,7 +153,7 @@ const router = useRouter()
const messageStore = useMessageStore()
// 主题
const theme = computed(() => messageStore.isDarkMode ? 'dark' : 'light')
const theme = computed(() => (messageStore.isDarkMode ? 'dark' : 'light'))
// 状态管理
const loading = ref(false)
@@ -196,8 +196,16 @@ const noneCategories = ref([])
// 颜色配置
const categoryColors = [
'#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7',
'#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E9'
'#FF6B6B',
'#4ECDC4',
'#45B7D1',
'#96CEB4',
'#FFEAA7',
'#DDA0DD',
'#98D8C8',
'#F7DC6F',
'#BB8FCE',
'#85C1E9'
]
// 计算属性
@@ -266,7 +274,6 @@ const loadStatistics = async () => {
// 加载分类统计
await loadCategoryStatistics(year, month)
} catch (error) {
console.error('加载统计数据失败:', error)
hasError.value = true
@@ -303,8 +310,8 @@ const loadMonthlyData = async (year, month) => {
if (dailyResult?.success && dailyResult.data) {
// 转换数据格式:添加完整的 date 字段
trendStats.value = dailyResult.data
.filter(item => item != null)
.map(item => ({
.filter((item) => item != null)
.map((item) => ({
date: `${year}-${month.toString().padStart(2, '0')}-${item.day.toString().padStart(2, '0')}`,
expense: item.expense || 0,
income: item.income || 0,
@@ -323,15 +330,18 @@ const loadYearlyData = async (year) => {
const trendResult = await getTrendStatistics({ startYear: year, startMonth: 1, monthCount: 12 })
if (trendResult?.success && trendResult.data) {
// 计算年度汇总
const yearTotal = trendResult.data.reduce((acc, item) => {
const expense = item.expense || 0
const income = item.income || 0
return {
totalExpense: acc.totalExpense + expense,
totalIncome: acc.totalIncome + income,
balance: acc.balance + income - expense
}
}, { totalExpense: 0, totalIncome: 0, balance: 0 })
const yearTotal = trendResult.data.reduce(
(acc, item) => {
const expense = item.expense || 0
const income = item.income || 0
return {
totalExpense: acc.totalExpense + expense,
totalIncome: acc.totalIncome + income,
balance: acc.balance + income - expense
}
},
{ totalExpense: 0, totalIncome: 0, balance: 0 }
)
monthlyStats.value = {
...yearTotal,
@@ -339,7 +349,7 @@ const loadYearlyData = async (year) => {
incomeCount: 0
}
trendStats.value = trendResult.data.map(item => ({
trendStats.value = trendResult.data.map((item) => ({
date: `${item.year}-${item.month.toString().padStart(2, '0')}-01`,
amount: (item.income || 0) - (item.expense || 0),
count: 1
@@ -371,7 +381,8 @@ const loadWeeklyData = async () => {
monthlyStats.value = {
totalExpense: weekSummaryResult.data.totalExpense || 0,
totalIncome: weekSummaryResult.data.totalIncome || 0,
balance: (weekSummaryResult.data.totalIncome || 0) - (weekSummaryResult.data.totalExpense || 0),
balance:
(weekSummaryResult.data.totalIncome || 0) - (weekSummaryResult.data.totalExpense || 0),
expenseCount: weekSummaryResult.data.expenseCount || 0,
incomeCount: weekSummaryResult.data.incomeCount || 0
}
@@ -450,7 +461,11 @@ const loadCategoryStatistics = async (year, month) => {
const currentColors = getChartColors()
// 处理支出分类结果
if (expenseResult.status === 'fulfilled' && expenseResult.value?.success && expenseResult.value.data) {
if (
expenseResult.status === 'fulfilled' &&
expenseResult.value?.success &&
expenseResult.value.data
) {
expenseCategories.value = expenseResult.value.data.map((item, index) => ({
classify: item.classify,
amount: item.amount || 0,
@@ -461,7 +476,11 @@ const loadCategoryStatistics = async (year, month) => {
}
// 处理收入分类结果
if (incomeResult.status === 'fulfilled' && incomeResult.value?.success && incomeResult.value.data) {
if (
incomeResult.status === 'fulfilled' &&
incomeResult.value?.success &&
incomeResult.value.data
) {
incomeCategories.value = incomeResult.value.data.map((item) => ({
classify: item.classify,
amount: item.amount || 0,
@@ -510,7 +529,11 @@ const loadCategoryStatistics = async (year, month) => {
const currentColors = getChartColors()
// 处理支出分类结果
if (expenseResult.status === 'fulfilled' && expenseResult.value?.success && expenseResult.value.data) {
if (
expenseResult.status === 'fulfilled' &&
expenseResult.value?.success &&
expenseResult.value.data
) {
expenseCategories.value = expenseResult.value.data.map((item, index) => ({
classify: item.classify,
amount: item.amount || 0,
@@ -521,7 +544,11 @@ const loadCategoryStatistics = async (year, month) => {
}
// 处理收入分类结果
if (incomeResult.status === 'fulfilled' && incomeResult.value?.success && incomeResult.value.data) {
if (
incomeResult.status === 'fulfilled' &&
incomeResult.value?.success &&
incomeResult.value.data
) {
incomeCategories.value = incomeResult.value.data.map((item) => ({
classify: item.classify,
amount: item.amount || 0,
@@ -618,8 +645,7 @@ const isLastPeriod = () => {
}
case 'month': {
// 比较年月
return current.getFullYear() === now.getFullYear() &&
current.getMonth() === now.getMonth()
return current.getFullYear() === now.getFullYear() && current.getMonth() === now.getMonth()
}
case 'year': {
// 比较年份
@@ -717,20 +743,14 @@ watch(currentPeriod, () => {
if (currentPeriod.value === 'year') {
selectedDate.value = [currentDate.value.getFullYear()]
} else {
selectedDate.value = [
currentDate.value.getFullYear(),
currentDate.value.getMonth() + 1
]
selectedDate.value = [currentDate.value.getFullYear(), currentDate.value.getMonth() + 1]
}
})
// 初始化
onMounted(() => {
// 设置默认选中日期
selectedDate.value = [
currentDate.value.getFullYear(),
currentDate.value.getMonth() + 1
]
selectedDate.value = [currentDate.value.getFullYear(), currentDate.value.getMonth() + 1]
loadStatistics()
})
</script>
@@ -792,4 +812,4 @@ onMounted(() => {
padding-bottom: calc(95px + env(safe-area-inset-bottom, 0px));
}
}
</style>
</style>

View File

@@ -101,7 +101,7 @@ const updateChart = () => {
if (props.period === 'week') {
// 周统计:直接使用传入的数据,按日期排序
chartData = [...props.data].sort((a, b) => new Date(a.date) - new Date(b.date))
xAxisLabels = chartData.map(item => {
xAxisLabels = chartData.map((item) => {
const date = new Date(item.date)
const weekDays = ['日', '一', '二', '三', '四', '五', '六']
return weekDays[date.getDay()]
@@ -121,14 +121,14 @@ const updateChart = () => {
// 创建完整的数据映射
const dataMap = new Map()
props.data.forEach(item => {
props.data.forEach((item) => {
if (item && item.date) {
dataMap.set(item.date, item)
}
})
// 生成完整的数据序列
chartData = allDays.map(date => {
chartData = allDays.map((date) => {
const dayData = dataMap.get(date)
return {
date,
@@ -141,9 +141,9 @@ const updateChart = () => {
} else if (props.period === 'year') {
// 年统计:直接使用数据,显示月份标签
chartData = [...props.data]
.filter(item => item && item.date)
.filter((item) => item && item.date)
.sort((a, b) => new Date(a.date) - new Date(b.date))
xAxisLabels = chartData.map(item => {
xAxisLabels = chartData.map((item) => {
const date = new Date(item.date)
return `${date.getMonth() + 1}`
})
@@ -153,27 +153,29 @@ const updateChart = () => {
if (chartData.length === 0) {
const option = {
backgroundColor: 'transparent',
graphic: [{
type: 'text',
left: 'center',
top: 'middle',
style: {
text: '暂无数据',
fontSize: 16,
fill: messageStore.isDarkMode ? '#9CA3AF' : '#6B7280'
graphic: [
{
type: 'text',
left: 'center',
top: 'middle',
style: {
text: '暂无数据',
fontSize: 16,
fill: messageStore.isDarkMode ? '#9CA3AF' : '#6B7280'
}
}
}]
]
}
chartInstance.setOption(option)
return
}
// 准备图表数据
const expenseData = chartData.map(item => {
const expenseData = chartData.map((item) => {
const amount = item.amount || 0
return amount < 0 ? Math.abs(amount) : 0
})
const incomeData = chartData.map(item => {
const incomeData = chartData.map((item) => {
const amount = item.amount || 0
return amount > 0 ? amount : 0
})
@@ -305,18 +307,25 @@ const updateChart = () => {
}
// 监听数据变化
watch(() => props.data, () => {
if (chartInstance) {
updateChart()
}
}, { deep: true })
watch(
() => props.data,
() => {
if (chartInstance) {
updateChart()
}
},
{ deep: true }
)
// 监听主题变化
watch(() => messageStore.isDarkMode, () => {
if (chartInstance) {
updateChart()
watch(
() => messageStore.isDarkMode,
() => {
if (chartInstance) {
updateChart()
}
}
})
)
onMounted(() => {
initChart()
@@ -358,4 +367,4 @@ onBeforeUnmount(() => {
width: 100%;
height: 180px;
}
</style>
</style>

View File

@@ -24,4 +24,4 @@ const emit = defineEmits(['category-click'])
const handleCategoryClick = (classify, type) => {
emit('category-click', classify, type)
}
</script>
</script>

View File

@@ -2,7 +2,7 @@
<!-- 支出分类统计 -->
<div
class="common-card"
style="padding-bottom: 10px;"
style="padding-bottom: 10px"
>
<div class="card-header">
<h3 class="card-title">
@@ -255,11 +255,15 @@ const renderPieChart = () => {
}
// 监听数据变化重新渲染图表
watch(() => [props.categories, props.totalExpense, props.colors], () => {
nextTick(() => {
renderPieChart()
})
}, { deep: true, immediate: true })
watch(
() => [props.categories, props.totalExpense, props.colors],
() => {
nextTick(() => {
renderPieChart()
})
},
{ deep: true, immediate: true }
)
// 组件销毁时清理图表实例
onBeforeUnmount(() => {
@@ -404,4 +408,4 @@ onBeforeUnmount(() => {
padding: 2px 8px;
border-radius: 10px;
}
</style>
</style>

View File

@@ -41,8 +41,8 @@ const props = defineProps({
})
const balanceClass = computed(() => ({
'positive': props.balance >= 0,
'negative': props.balance < 0
positive: props.balance >= 0,
negative: props.balance < 0
}))
</script>
@@ -90,4 +90,4 @@ const balanceClass = computed(() => ({
}
}
}
</style>
</style>

View File

@@ -24,4 +24,4 @@ const emit = defineEmits(['category-click'])
const handleCategoryClick = (classify, type) => {
emit('category-click', classify, type)
}
</script>
</script>

View File

@@ -269,4 +269,4 @@ const noneCategories = computed(() => {
.none-text {
color: var(--van-gray-6);
}
</style>
</style>

View File

@@ -80,8 +80,8 @@ let chartInstance = null
// 计算结余样式类
const balanceClass = computed(() => ({
'positive': props.balance >= 0,
'negative': props.balance < 0
positive: props.balance >= 0,
negative: props.balance < 0
}))
// 计算图表标题
@@ -152,7 +152,7 @@ const updateChart = () => {
if (props.period === 'week') {
// 周统计:直接使用传入的数据,按日期排序
chartData = [...props.trendData].sort((a, b) => new Date(a.date) - new Date(b.date))
xAxisLabels = chartData.map(item => {
xAxisLabels = chartData.map((item) => {
const date = new Date(item.date)
const weekDays = ['日', '一', '二', '三', '四', '五', '六']
return weekDays[date.getDay()]
@@ -172,14 +172,14 @@ const updateChart = () => {
// 创建完整的数据映射
const dataMap = new Map()
props.trendData.forEach(item => {
props.trendData.forEach((item) => {
if (item && item.date) {
dataMap.set(item.date, item)
}
})
// 生成完整的数据序列
chartData = allDays.map(date => {
chartData = allDays.map((date) => {
const dayData = dataMap.get(date)
return {
date,
@@ -193,9 +193,9 @@ const updateChart = () => {
} else if (props.period === 'year') {
// 年统计:直接使用数据,显示月份标签
chartData = [...props.trendData]
.filter(item => item && item.date)
.filter((item) => item && item.date)
.sort((a, b) => new Date(a.date) - new Date(b.date))
xAxisLabels = chartData.map(item => {
xAxisLabels = chartData.map((item) => {
const date = new Date(item.date)
return `${date.getMonth() + 1}`
})
@@ -205,16 +205,18 @@ const updateChart = () => {
if (chartData.length === 0) {
const option = {
backgroundColor: 'transparent',
graphic: [{
type: 'text',
left: 'center',
top: 'middle',
style: {
text: '暂无数据',
fontSize: 16,
fill: messageStore.isDarkMode ? '#9CA3AF' : '#6B7280'
graphic: [
{
type: 'text',
left: 'center',
top: 'middle',
style: {
text: '暂无数据',
fontSize: 16,
fill: messageStore.isDarkMode ? '#9CA3AF' : '#6B7280'
}
}
}]
]
}
chartInstance.setOption(option)
return
@@ -227,7 +229,7 @@ const updateChart = () => {
const expenseData = []
const incomeData = []
chartData.forEach(item => {
chartData.forEach((item) => {
// 支持两种数据格式1) expense/income字段 2) amount字段兼容旧数据
let expense = 0
let income = 0
@@ -401,18 +403,25 @@ const updateChart = () => {
}
// 监听数据变化
watch(() => props.trendData, () => {
if (chartInstance) {
updateChart()
}
}, { deep: true })
watch(
() => props.trendData,
() => {
if (chartInstance) {
updateChart()
}
},
{ deep: true }
)
// 监听主题变化
watch(() => messageStore.isDarkMode, () => {
if (chartInstance) {
updateChart()
watch(
() => messageStore.isDarkMode,
() => {
if (chartInstance) {
updateChart()
}
}
})
)
onMounted(() => {
initChart()