1
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 22s
Docker Build & Deploy / Deploy to Production (push) Successful in 6s
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 22s
Docker Build & Deploy / Deploy to Production (push) Successful in 6s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 1s
This commit is contained in:
@@ -42,12 +42,51 @@
|
||||
class="statistics-content"
|
||||
>
|
||||
<div>
|
||||
<!-- 余额卡片 -->
|
||||
<div
|
||||
class="common-card"
|
||||
style="margin-top: 12px;"
|
||||
>
|
||||
<div
|
||||
class="card-header"
|
||||
style="padding-bottom: 0;"
|
||||
>
|
||||
<h3 class="card-title">
|
||||
余额
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<!-- 余额金额 -->
|
||||
<div class="balance-amount">
|
||||
<span
|
||||
class="balance-value"
|
||||
:class="{ 'balance-positive': displayBalance >= 0, 'balance-negative': displayBalance < 0 }"
|
||||
>
|
||||
¥{{ formatMoney(displayBalance) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 余额变化图表 -->
|
||||
<div
|
||||
class="balance-chart"
|
||||
style="height: 130px; padding: 0"
|
||||
>
|
||||
<div
|
||||
ref="balanceChartRef"
|
||||
style="width: 100%; height: 100%"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 趋势统计 -->
|
||||
<div
|
||||
class="common-card"
|
||||
style="padding-bottom: 5px; margin-top: 12px;"
|
||||
>
|
||||
<div class="card-header">
|
||||
<div
|
||||
class="card-header"
|
||||
style="padding-bottom: 0;"
|
||||
>
|
||||
<h3 class="card-title">
|
||||
收支趋势
|
||||
</h3>
|
||||
@@ -288,7 +327,7 @@ import { ref, computed, onMounted, onActivated, nextTick, watch } from 'vue'
|
||||
import { onBeforeUnmount } from 'vue'
|
||||
import { showToast } from 'vant'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { getMonthlyStatistics, getCategoryStatistics, getDailyStatistics } from '@/api/statistics'
|
||||
import { getMonthlyStatistics, getCategoryStatistics, getDailyStatistics, getBalanceStatistics } from '@/api/statistics'
|
||||
import * as echarts from 'echarts'
|
||||
import { getTransactionList, getTransactionDetail } from '@/api/transactionRecord'
|
||||
import TransactionList from '@/components/TransactionList.vue'
|
||||
@@ -392,10 +431,14 @@ 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
|
||||
|
||||
// 日期范围
|
||||
const minDate = new Date(2020, 0, 1)
|
||||
@@ -425,6 +468,50 @@ const isUnclassified = computed(() => {
|
||||
return selectedClassify.value === '未分类' || selectedClassify.value === ''
|
||||
})
|
||||
|
||||
// 当月累积余额
|
||||
const currentMonthBalance = computed(() => {
|
||||
if (balanceData.value.length === 0) {
|
||||
return 0
|
||||
}
|
||||
// 获取最后一天的累积余额
|
||||
return balanceData.value[balanceData.value.length - 1].cumulativeBalance || 0
|
||||
})
|
||||
|
||||
// 显示的动画余额
|
||||
const displayBalance = ref(0)
|
||||
|
||||
// 监听余额变化,执行动画
|
||||
watch(currentMonthBalance, (newVal, oldVal) => {
|
||||
if (oldVal === undefined) {
|
||||
// 初始加载,直接设置,不需要动画
|
||||
displayBalance.value = newVal
|
||||
return
|
||||
}
|
||||
|
||||
// 数字跳动动画
|
||||
const duration = 800 // 动画持续时间(毫秒)
|
||||
const startValue = oldVal
|
||||
const endValue = newVal
|
||||
const startTime = Date.now()
|
||||
|
||||
const animate = () => {
|
||||
const elapsed = Date.now() - startTime
|
||||
const progress = Math.min(elapsed / duration, 1)
|
||||
|
||||
// 使用缓动函数(easeOutQuad)
|
||||
const easeProgress = 1 - (1 - progress) * (1 - progress)
|
||||
displayBalance.value = Math.round(startValue + (endValue - startValue) * easeProgress)
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(animate)
|
||||
} else {
|
||||
displayBalance.value = endValue
|
||||
}
|
||||
}
|
||||
|
||||
animate()
|
||||
})
|
||||
|
||||
// 格式化金额
|
||||
const formatMoney = (value) => {
|
||||
if (!value && value !== 0) {
|
||||
@@ -463,7 +550,7 @@ const fetchStatistics = async (showLoading = true) => {
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all([fetchMonthlyData(), fetchCategoryData(), fetchDailyData()])
|
||||
await Promise.all([fetchMonthlyData(), fetchCategoryData(), fetchDailyData(), fetchBalanceData()])
|
||||
} catch (error) {
|
||||
console.error('获取统计数据失败:', error)
|
||||
showToast('获取统计数据失败')
|
||||
@@ -474,6 +561,7 @@ const fetchStatistics = async (showLoading = true) => {
|
||||
nextTick(() => {
|
||||
renderChart(dailyData.value)
|
||||
renderPieChart()
|
||||
renderBalanceChart()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -576,6 +664,23 @@ const fetchDailyData = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取余额统计数据(独立接口)
|
||||
const fetchBalanceData = async () => {
|
||||
try {
|
||||
const response = await getBalanceStatistics({
|
||||
year: currentYear.value,
|
||||
month: currentMonth.value
|
||||
})
|
||||
|
||||
if (response.success && response.data) {
|
||||
balanceData.value = response.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取余额统计数据失败:', error)
|
||||
showToast('获取余额统计数据失败')
|
||||
}
|
||||
}
|
||||
|
||||
const renderChart = (data) => {
|
||||
if (!chartRef.value) {
|
||||
return
|
||||
@@ -670,6 +775,12 @@ const renderChart = (data) => {
|
||||
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',
|
||||
@@ -682,10 +793,14 @@ const renderChart = (data) => {
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: ['支出', '收入', '存款'],
|
||||
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: {
|
||||
@@ -876,6 +991,121 @@ const renderPieChart = () => {
|
||||
pieChartInstance.setOption(option)
|
||||
}
|
||||
|
||||
// 渲染余额变化图表
|
||||
const renderBalanceChart = () => {
|
||||
if (!balanceChartRef.value) {
|
||||
return
|
||||
}
|
||||
if (balanceData.value.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// 尝试获取DOM上的现有实例
|
||||
const existingInstance = echarts.getInstanceByDom(balanceChartRef.value)
|
||||
|
||||
if (balanceChartInstance && balanceChartInstance !== existingInstance) {
|
||||
if (!balanceChartInstance.isDisposed()) {
|
||||
balanceChartInstance.dispose()
|
||||
}
|
||||
balanceChartInstance = null
|
||||
}
|
||||
|
||||
if (balanceChartInstance && balanceChartInstance.getDom() !== balanceChartRef.value) {
|
||||
balanceChartInstance.dispose()
|
||||
balanceChartInstance = null
|
||||
}
|
||||
|
||||
if (!balanceChartInstance && existingInstance) {
|
||||
balanceChartInstance = existingInstance
|
||||
}
|
||||
|
||||
if (!balanceChartInstance) {
|
||||
balanceChartInstance = echarts.init(balanceChartRef.value)
|
||||
}
|
||||
|
||||
const dates = balanceData.value.map((item) => {
|
||||
const date = new Date(item.date)
|
||||
return `${date.getMonth() + 1}/${date.getDate()}`
|
||||
})
|
||||
|
||||
const balances = balanceData.value.map((item) => item.cumulativeBalance)
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: function (params) {
|
||||
if (params.length === 0) {
|
||||
return ''
|
||||
}
|
||||
const param = params[0]
|
||||
return `${param.name}<br/>余额: ¥${formatMoney(param.value)}`
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '5%',
|
||||
top: '5%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: dates,
|
||||
axisLabel: {
|
||||
color: getCssVar('--chart-text-muted'),
|
||||
fontSize: 11
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
splitNumber: 4,
|
||||
axisLabel: {
|
||||
color: getCssVar('--chart-text-muted'),
|
||||
fontSize: 11,
|
||||
formatter: (value) => {
|
||||
return value / 1000 + 'k'
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
type: 'dashed',
|
||||
color: getCssVar('--van-border-color')
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '余额',
|
||||
type: 'line',
|
||||
data: balances,
|
||||
itemStyle: { color: getCssVar('--chart-color-13') },
|
||||
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')
|
||||
}
|
||||
])
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
balanceChartInstance.setOption(option)
|
||||
// 设置图表透明度
|
||||
if (balanceChartRef.value) {
|
||||
balanceChartRef.value.style.opacity = '0.85'
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转到智能分析页面
|
||||
const goToAnalysis = () => {
|
||||
router.push('/bill-analysis')
|
||||
@@ -1061,6 +1291,7 @@ onMounted(() => {
|
||||
const handleResize = () => {
|
||||
chartInstance && chartInstance.resize()
|
||||
pieChartInstance && pieChartInstance.resize()
|
||||
balanceChartInstance && balanceChartInstance.resize()
|
||||
}
|
||||
|
||||
// 监听DOM引用变化,确保在月份切换DOM重建后重新渲染图表
|
||||
@@ -1085,6 +1316,15 @@ watch(pieChartRef, (newVal) => {
|
||||
}
|
||||
})
|
||||
|
||||
watch(balanceChartRef, (newVal) => {
|
||||
if (newVal) {
|
||||
setTimeout(() => {
|
||||
renderBalanceChart()
|
||||
balanceChartInstance && balanceChartInstance.resize()
|
||||
}, 50)
|
||||
}
|
||||
})
|
||||
|
||||
// 页面激活时刷新数据(从其他页面返回时)
|
||||
onActivated(() => {
|
||||
fetchStatistics()
|
||||
@@ -1105,6 +1345,7 @@ onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
chartInstance && chartInstance.dispose()
|
||||
pieChartInstance && pieChartInstance.dispose()
|
||||
balanceChartInstance && balanceChartInstance.dispose()
|
||||
})
|
||||
|
||||
const onGlobalTransactionsChanged = () => {
|
||||
@@ -1145,6 +1386,37 @@ onBeforeUnmount(() => {
|
||||
color: var(--van-text-color);
|
||||
}
|
||||
|
||||
/* 余额卡片 */
|
||||
.balance-amount {
|
||||
text-align: center;
|
||||
padding: 9px 0 8px 0;
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
color: var(--van-text-color);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text",
|
||||
"Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
.balance-value {
|
||||
color: var(--chart-color-13);
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.balance-value.balance-positive {
|
||||
color: var(--van-success-color);
|
||||
}
|
||||
|
||||
.balance-value.balance-negative {
|
||||
color: var(--van-danger-color);
|
||||
}
|
||||
|
||||
.balance-chart {
|
||||
border-radius: 8px;
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
padding: 10px 0;
|
||||
margin: 0 -12px;
|
||||
}
|
||||
|
||||
/* 环形图 */
|
||||
.chart-container {
|
||||
padding: 0;
|
||||
@@ -1411,8 +1683,8 @@ onBeforeUnmount(() => {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.side-by-side-cards .card-header {
|
||||
margin-bottom: 12px;
|
||||
.card-header {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.text-ellipsis {
|
||||
|
||||
Reference in New Issue
Block a user