fix
This commit is contained in:
@@ -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('')
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -24,4 +24,4 @@ const emit = defineEmits(['category-click'])
|
||||
const handleCategoryClick = (classify, type) => {
|
||||
emit('category-click', classify, type)
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -24,4 +24,4 @@ const emit = defineEmits(['category-click'])
|
||||
const handleCategoryClick = (classify, type) => {
|
||||
emit('category-click', classify, type)
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -269,4 +269,4 @@ const noneCategories = computed(() => {
|
||||
.none-text {
|
||||
color: var(--van-gray-6);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user