fix
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 31s
Docker Build & Deploy / Deploy to Production (push) Successful in 7s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 1s
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 31s
Docker Build & Deploy / Deploy to Production (push) Successful in 7s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 1s
This commit is contained in:
@@ -66,10 +66,10 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 余额变化图表 -->
|
||||
<!-- 余额变化图表(融合收支趋势) -->
|
||||
<div
|
||||
class="balance-chart"
|
||||
style="height: 130px; padding: 0"
|
||||
style="height: 190px; padding: 0"
|
||||
>
|
||||
<div
|
||||
ref="balanceChartRef"
|
||||
@@ -77,31 +77,6 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 趋势统计 -->
|
||||
<div
|
||||
class="common-card"
|
||||
style="padding-bottom: 5px; margin-top: 12px;"
|
||||
>
|
||||
<div
|
||||
class="card-header"
|
||||
style="padding-bottom: 0;"
|
||||
>
|
||||
<h3 class="card-title">
|
||||
收支趋势
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="trend-chart"
|
||||
style="height: 240px; padding: 10px 0"
|
||||
>
|
||||
<div
|
||||
ref="chartRef"
|
||||
style="width: 100%; height: 100%"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 分类统计 -->
|
||||
<div
|
||||
class="common-card"
|
||||
@@ -450,10 +425,8 @@ const noneCategoriesView = computed(() => {
|
||||
const dailyData = ref([])
|
||||
// 余额数据(独立)
|
||||
const balanceData = ref([])
|
||||
const chartRef = ref(null)
|
||||
const pieChartRef = ref(null)
|
||||
const balanceChartRef = ref(null)
|
||||
let chartInstance = null
|
||||
let pieChartInstance = null
|
||||
let balanceChartInstance = null
|
||||
|
||||
@@ -576,7 +549,6 @@ const fetchStatistics = async (showLoading = true) => {
|
||||
firstLoading.value = false
|
||||
// DOM 更新后渲染图表
|
||||
nextTick(() => {
|
||||
renderChart(dailyData.value)
|
||||
renderPieChart()
|
||||
renderBalanceChart()
|
||||
})
|
||||
@@ -671,7 +643,7 @@ const fetchDailyData = async () => {
|
||||
// 如果不是首次加载(即DOM已存在),直接渲染
|
||||
if (!firstLoading.value) {
|
||||
nextTick(() => {
|
||||
renderChart(response.data)
|
||||
renderBalanceChart()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -691,6 +663,12 @@ const fetchBalanceData = async () => {
|
||||
|
||||
if (response.success && response.data) {
|
||||
balanceData.value = response.data
|
||||
// 如果不是首次加载,重新渲染余额图表
|
||||
if (!firstLoading.value) {
|
||||
nextTick(() => {
|
||||
renderBalanceChart()
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取余额统计数据失败:', error)
|
||||
@@ -698,193 +676,6 @@ const fetchBalanceData = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const renderChart = (data) => {
|
||||
if (!chartRef.value) {
|
||||
return
|
||||
}
|
||||
|
||||
// 尝试获取DOM上的现有实例
|
||||
const existingInstance = echarts.getInstanceByDom(chartRef.value)
|
||||
|
||||
// 如果当前保存的实例与DOM不一致,或者DOM上已经有实例但我们没保存引用
|
||||
if (chartInstance && chartInstance !== existingInstance) {
|
||||
// 这种情况很少见,但为了保险,销毁旧的引用
|
||||
if (!chartInstance.isDisposed()) {
|
||||
chartInstance.dispose()
|
||||
}
|
||||
chartInstance = null
|
||||
}
|
||||
|
||||
// 如果DOM变了(transition导致的),旧的chartInstance绑定的DOM已经不在了
|
||||
// 这时 chartInstance.getDom() !== chartRef.value
|
||||
if (chartInstance && chartInstance.getDom() !== chartRef.value) {
|
||||
chartInstance.dispose()
|
||||
chartInstance = null
|
||||
}
|
||||
|
||||
// 如果DOM上已经有实例(可能由其他途径创建),复用它
|
||||
if (!chartInstance && existingInstance) {
|
||||
chartInstance = existingInstance
|
||||
}
|
||||
|
||||
if (!chartInstance) {
|
||||
chartInstance = echarts.init(chartRef.value)
|
||||
}
|
||||
|
||||
// 补全当月所有日期
|
||||
const now = new Date()
|
||||
let daysInMonth
|
||||
|
||||
if (currentYear.value === now.getFullYear() && currentMonth.value === now.getMonth() + 1) {
|
||||
// 如果是当前月,只显示到今天
|
||||
daysInMonth = now.getDate()
|
||||
} else {
|
||||
// 如果是过去月,显示整月
|
||||
daysInMonth = new Date(currentYear.value, currentMonth.value, 0).getDate()
|
||||
}
|
||||
|
||||
const fullData = []
|
||||
|
||||
// 创建日期映射
|
||||
const dataMap = new Map()
|
||||
data.forEach((item) => {
|
||||
const day = new Date(item.date).getDate()
|
||||
dataMap.set(day, item)
|
||||
})
|
||||
|
||||
for (let i = 1; i <= daysInMonth; i++) {
|
||||
const item = dataMap.get(i)
|
||||
if (item) {
|
||||
fullData.push(item)
|
||||
} else {
|
||||
fullData.push({
|
||||
date: `${currentYear.value}-${String(currentMonth.value).padStart(2, '0')}-${String(i).padStart(2, '0')}`,
|
||||
count: 0,
|
||||
expense: 0,
|
||||
income: 0,
|
||||
balance: 0
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const dates = fullData.map((item) => {
|
||||
const date = new Date(item.date)
|
||||
return `${date.getDate()}日`
|
||||
})
|
||||
|
||||
// Calculate cumulative values
|
||||
let accumulatedExpense = 0
|
||||
let accumulatedIncome = 0
|
||||
let accumulatedBalance = 0
|
||||
|
||||
const expenses = fullData.map((item) => {
|
||||
accumulatedExpense += item.expense
|
||||
return accumulatedExpense
|
||||
})
|
||||
|
||||
const incomes = fullData.map((item) => {
|
||||
accumulatedIncome += item.income
|
||||
return accumulatedIncome
|
||||
})
|
||||
|
||||
const balances = fullData.map((item) => {
|
||||
accumulatedBalance += item.balance
|
||||
return accumulatedBalance
|
||||
})
|
||||
|
||||
const legendData = [
|
||||
{ name: '支出', value: '¥' + formatMoney(expenses[expenses.length - 1]) },
|
||||
{ name: '收入', value: '¥' + formatMoney(incomes[incomes.length - 1]) },
|
||||
{ name: '存款', value: '¥' + formatMoney(balances[balances.length - 1]) }
|
||||
]
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: function (params) {
|
||||
let result = params[0].name + '<br/>'
|
||||
params.forEach((param) => {
|
||||
result += param.marker + param.seriesName + ': ' + formatMoney(param.value) + '<br/>'
|
||||
})
|
||||
return result
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: legendData.map((item) => item.name),
|
||||
bottom: 0,
|
||||
textStyle: {
|
||||
color: getCssVar('--chart-text-muted') // 适配深色模式
|
||||
},
|
||||
formatter: function (name) {
|
||||
const item = legendData.find((d) => d.name === name)
|
||||
return item ? `${name} ${item.value}` : name
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '15%',
|
||||
top: '5%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: dates,
|
||||
axisLabel: {
|
||||
color: getCssVar('--chart-text-muted') // 适配深色模式
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
splitNumber: 5,
|
||||
axisLabel: {
|
||||
color: getCssVar('--chart-text-muted'), // 适配深色模式
|
||||
formatter: (value) => {
|
||||
return value / 1000 + 'k'
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
type: 'dashed',
|
||||
color: getCssVar('--van-border-color') // 深色分割线
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '支出',
|
||||
type: 'line',
|
||||
data: expenses,
|
||||
itemStyle: { color: getCssVar('--chart-color-1') },
|
||||
showSymbol: false,
|
||||
smooth: true,
|
||||
lineStyle: { width: 2 }
|
||||
},
|
||||
{
|
||||
name: '收入',
|
||||
type: 'line',
|
||||
data: incomes,
|
||||
itemStyle: { color: getCssVar('--chart-color-2') },
|
||||
showSymbol: false,
|
||||
smooth: true,
|
||||
lineStyle: { width: 2 }
|
||||
},
|
||||
{
|
||||
name: '存款',
|
||||
type: 'line',
|
||||
data: balances,
|
||||
itemStyle: { color: getCssVar('--chart-color-13') },
|
||||
showSymbol: false,
|
||||
smooth: true,
|
||||
lineStyle: { width: 2 }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
chartInstance.setOption(option)
|
||||
}
|
||||
|
||||
const renderPieChart = () => {
|
||||
if (!pieChartRef.value) {
|
||||
return
|
||||
@@ -1010,12 +801,12 @@ const renderPieChart = () => {
|
||||
pieChartInstance.setOption(option)
|
||||
}
|
||||
|
||||
// 渲染余额变化图表
|
||||
// 渲染余额变化图表(融合支出、收入、余额三条线)
|
||||
const renderBalanceChart = () => {
|
||||
if (!balanceChartRef.value) {
|
||||
return
|
||||
}
|
||||
if (balanceData.value.length === 0) {
|
||||
if (balanceData.value.length === 0 && dailyData.value.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1042,28 +833,168 @@ const renderBalanceChart = () => {
|
||||
balanceChartInstance = echarts.init(balanceChartRef.value)
|
||||
}
|
||||
|
||||
const dates = balanceData.value.map((item) => {
|
||||
const date = new Date(item.date)
|
||||
return `${date.getMonth() + 1}/${date.getDate()}`
|
||||
})
|
||||
// 判断是年度统计还是月度统计
|
||||
const isYearlyView = currentMonth.value === 0
|
||||
let dates, expenses, incomes, balances
|
||||
|
||||
const balances = balanceData.value.map((item) => item.cumulativeBalance)
|
||||
if (isYearlyView) {
|
||||
// 按年统计:按月聚合数据
|
||||
const monthlyMap = new Map()
|
||||
const balanceMonthlyMap = new Map()
|
||||
|
||||
// 聚合 dailyData 按月
|
||||
dailyData.value.forEach((item) => {
|
||||
const date = new Date(item.date)
|
||||
const month = date.getMonth() + 1 // 1-12
|
||||
if (!monthlyMap.has(month)) {
|
||||
monthlyMap.set(month, { expense: 0, income: 0 })
|
||||
}
|
||||
const data = monthlyMap.get(month)
|
||||
data.expense += item.expense
|
||||
data.income += item.income
|
||||
})
|
||||
|
||||
// 聚合 balanceData 按月(取每月最后一天的余额)
|
||||
balanceData.value.forEach((item) => {
|
||||
const date = new Date(item.date)
|
||||
const month = date.getMonth() + 1
|
||||
const day = date.getDate()
|
||||
|
||||
if (!balanceMonthlyMap.has(month) || day > balanceMonthlyMap.get(month).day) {
|
||||
balanceMonthlyMap.set(month, { balance: item.cumulativeBalance, day })
|
||||
}
|
||||
})
|
||||
|
||||
// 构建12个月的完整数据
|
||||
const now = new Date()
|
||||
const currentMonthNum = now.getFullYear() === currentYear.value ? now.getMonth() + 1 : 12
|
||||
|
||||
dates = []
|
||||
const monthlyExpenses = []
|
||||
const monthlyIncomes = []
|
||||
const monthlyBalances = []
|
||||
|
||||
let accumulatedExpense = 0
|
||||
let accumulatedIncome = 0
|
||||
|
||||
for (let m = 1; m <= currentMonthNum; m++) {
|
||||
dates.push(`${m}月`)
|
||||
|
||||
const data = monthlyMap.get(m) || { expense: 0, income: 0 }
|
||||
accumulatedExpense += data.expense
|
||||
accumulatedIncome += data.income
|
||||
|
||||
monthlyExpenses.push(accumulatedExpense)
|
||||
monthlyIncomes.push(accumulatedIncome)
|
||||
|
||||
const balanceData = balanceMonthlyMap.get(m)
|
||||
monthlyBalances.push(balanceData ? balanceData.balance : 0)
|
||||
}
|
||||
|
||||
expenses = monthlyExpenses
|
||||
incomes = monthlyIncomes
|
||||
balances = monthlyBalances
|
||||
|
||||
} else {
|
||||
// 按月统计:按日显示
|
||||
const now = new Date()
|
||||
let daysInMonth
|
||||
|
||||
if (currentYear.value === now.getFullYear() && currentMonth.value === now.getMonth() + 1) {
|
||||
daysInMonth = now.getDate()
|
||||
} else {
|
||||
daysInMonth = new Date(currentYear.value, currentMonth.value, 0).getDate()
|
||||
}
|
||||
|
||||
const fullData = []
|
||||
const dataMap = new Map()
|
||||
dailyData.value.forEach((item) => {
|
||||
const day = new Date(item.date).getDate()
|
||||
dataMap.set(day, item)
|
||||
})
|
||||
|
||||
// 创建余额映射
|
||||
const balanceMap = new Map()
|
||||
if (balanceData.value && balanceData.value.length > 0) {
|
||||
balanceData.value.forEach((item) => {
|
||||
const day = new Date(item.date).getDate()
|
||||
balanceMap.set(day, item.cumulativeBalance)
|
||||
})
|
||||
}
|
||||
|
||||
for (let i = 1; i <= daysInMonth; i++) {
|
||||
const item = dataMap.get(i)
|
||||
if (item) {
|
||||
fullData.push(item)
|
||||
} else {
|
||||
fullData.push({
|
||||
date: `${currentYear.value}-${String(currentMonth.value).padStart(2, '0')}-${String(i).padStart(2, '0')}`,
|
||||
count: 0,
|
||||
expense: 0,
|
||||
income: 0,
|
||||
balance: 0
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
dates = fullData.map((item) => {
|
||||
const date = new Date(item.date)
|
||||
return `${date.getDate()}日`
|
||||
})
|
||||
|
||||
// 计算累计支出和收入
|
||||
let accumulatedExpense = 0
|
||||
let accumulatedIncome = 0
|
||||
|
||||
expenses = fullData.map((item) => {
|
||||
accumulatedExpense += item.expense
|
||||
return accumulatedExpense
|
||||
})
|
||||
|
||||
incomes = fullData.map((item) => {
|
||||
accumulatedIncome += item.income
|
||||
return accumulatedIncome
|
||||
})
|
||||
|
||||
// 使用余额接口数据
|
||||
balances = fullData.map((item, index) => {
|
||||
const day = index + 1
|
||||
return balanceMap.get(day) || 0
|
||||
})
|
||||
}
|
||||
|
||||
const legendData = [
|
||||
{ name: '支出', value: '¥' + formatMoney(expenses[expenses.length - 1]) },
|
||||
{ name: '收入', value: '¥' + formatMoney(incomes[incomes.length - 1]) },
|
||||
{ name: '余额', value: '¥' + formatMoney(balances[balances.length - 1]) }
|
||||
]
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: function (params) {
|
||||
if (params.length === 0) {
|
||||
return ''
|
||||
}
|
||||
const param = params[0]
|
||||
return `${param.name}<br/>余额: ¥${formatMoney(param.value)}`
|
||||
let result = params[0].name + '<br/>'
|
||||
params.forEach((param) => {
|
||||
result += param.marker + param.seriesName + ': ¥' + formatMoney(param.value) + '<br/>'
|
||||
})
|
||||
return result
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: legendData.map((item) => item.name),
|
||||
bottom: 0,
|
||||
textStyle: {
|
||||
color: getCssVar('--chart-text-muted')
|
||||
},
|
||||
formatter: function (name) {
|
||||
const item = legendData.find((d) => d.name === name)
|
||||
return item ? `${name} ${item.value}` : name
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '5%',
|
||||
bottom: '15%',
|
||||
top: '5%',
|
||||
containLabel: true
|
||||
},
|
||||
@@ -1094,35 +1025,37 @@ const renderBalanceChart = () => {
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '支出',
|
||||
type: 'line',
|
||||
data: expenses,
|
||||
itemStyle: { color: '#ff6b6b' },
|
||||
showSymbol: false,
|
||||
smooth: true,
|
||||
lineStyle: { width: 2 }
|
||||
},
|
||||
{
|
||||
name: '收入',
|
||||
type: 'line',
|
||||
data: incomes,
|
||||
itemStyle: { color: '#51cf66' },
|
||||
showSymbol: false,
|
||||
smooth: true,
|
||||
lineStyle: { width: 2 }
|
||||
},
|
||||
{
|
||||
name: '余额',
|
||||
type: 'line',
|
||||
data: balances,
|
||||
itemStyle: { color: getCssVar('--chart-color-13') },
|
||||
itemStyle: { color: '#4c9cf1' },
|
||||
showSymbol: false,
|
||||
smooth: true,
|
||||
lineStyle: { width: 2 },
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{
|
||||
offset: 0,
|
||||
color: getCssVar('--chart-color-13')
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: getCssVar('--chart-color-13')
|
||||
}
|
||||
])
|
||||
}
|
||||
lineStyle: { width: 2 }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
balanceChartInstance.setOption(option)
|
||||
// 设置图表透明度
|
||||
if (balanceChartRef.value) {
|
||||
balanceChartRef.value.style.opacity = '0.85'
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转到智能分析页面
|
||||
@@ -1308,24 +1241,11 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
const handleResize = () => {
|
||||
chartInstance && chartInstance.resize()
|
||||
pieChartInstance && pieChartInstance.resize()
|
||||
balanceChartInstance && balanceChartInstance.resize()
|
||||
}
|
||||
|
||||
// 监听DOM引用变化,确保在月份切换DOM重建后重新渲染图表
|
||||
watch(chartRef, (newVal) => {
|
||||
// 无论有没有数据,只要DOM变了,就尝试渲染
|
||||
// 如果没有数据,renderChart 内部也应该处理(或者我们可以传空数据)
|
||||
if (newVal) {
|
||||
setTimeout(() => {
|
||||
// 传入当前 dailyData,即使是空的,renderChart 应该能处理
|
||||
renderChart(dailyData.value || [])
|
||||
chartInstance && chartInstance.resize()
|
||||
}, 50)
|
||||
}
|
||||
})
|
||||
|
||||
watch(pieChartRef, (newVal) => {
|
||||
if (newVal) {
|
||||
setTimeout(() => {
|
||||
@@ -1362,7 +1282,6 @@ onBeforeUnmount(() => {
|
||||
window.removeEventListener &&
|
||||
window.removeEventListener('transaction-deleted', onGlobalTransactionDeleted)
|
||||
window.removeEventListener('resize', handleResize)
|
||||
chartInstance && chartInstance.dispose()
|
||||
pieChartInstance && pieChartInstance.dispose()
|
||||
balanceChartInstance && balanceChartInstance.dispose()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user