All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 17s
Docker Build & Deploy / Deploy to Production (push) Successful in 8s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 1s
273 lines
6.9 KiB
Vue
273 lines
6.9 KiB
Vue
<template>
|
|
<div class="daily-trend-card common-card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">
|
|
{{ chartTitle }} (收支)
|
|
</h3>
|
|
</div>
|
|
|
|
<div class="trend-chart">
|
|
<BaseChart
|
|
type="line"
|
|
:data="chartData"
|
|
:options="chartOptions"
|
|
:loading="false"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed } from 'vue'
|
|
import BaseChart from '@/components/Charts/BaseChart.vue'
|
|
import { useChartTheme } from '@/composables/useChartTheme'
|
|
import { createGradient } from '@/utils/chartHelpers'
|
|
|
|
const props = defineProps({
|
|
data: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
period: {
|
|
type: String,
|
|
default: 'month'
|
|
},
|
|
currentDate: {
|
|
type: Date,
|
|
required: true
|
|
}
|
|
})
|
|
|
|
// Chart.js 相关
|
|
const { getChartOptions } = useChartTheme()
|
|
|
|
// 计算图表标题
|
|
const chartTitle = computed(() => {
|
|
switch (props.period) {
|
|
case 'week':
|
|
return '每日趋势'
|
|
case 'month':
|
|
return '每日趋势'
|
|
case 'year':
|
|
return '每月趋势'
|
|
default:
|
|
return '趋势'
|
|
}
|
|
})
|
|
|
|
// 获取月份天数
|
|
const getDaysInMonth = (year, month) => {
|
|
return new Date(year, month, 0).getDate()
|
|
}
|
|
|
|
// 准备图表数据(通用)
|
|
const prepareChartData = () => {
|
|
let chartData = []
|
|
let xAxisLabels = []
|
|
|
|
if (props.period === 'week') {
|
|
chartData = [...props.data].sort((a, b) => new Date(a.date) - new Date(b.date))
|
|
xAxisLabels = chartData.map((item) => {
|
|
const date = new Date(item.date)
|
|
const weekDays = ['日', '一', '二', '三', '四', '五', '六']
|
|
return weekDays[date.getDay()]
|
|
})
|
|
} else if (props.period === 'month') {
|
|
const currentDate = props.currentDate
|
|
const year = currentDate.getFullYear()
|
|
const month = currentDate.getMonth() + 1
|
|
const daysInMonth = getDaysInMonth(year, month)
|
|
|
|
const allDays = Array.from({ length: daysInMonth }, (_, i) => {
|
|
const day = i + 1
|
|
const paddedDay = day.toString().padStart(2, '0')
|
|
return `${year}-${month.toString().padStart(2, '0')}-${paddedDay}`
|
|
})
|
|
|
|
const dataMap = new Map()
|
|
props.data.forEach((item) => {
|
|
if (item && item.date) {
|
|
dataMap.set(item.date, item)
|
|
}
|
|
})
|
|
|
|
chartData = allDays.map((date) => {
|
|
const dayData = dataMap.get(date)
|
|
return {
|
|
date,
|
|
amount: dayData?.amount || 0,
|
|
count: dayData?.count || 0
|
|
}
|
|
})
|
|
|
|
xAxisLabels = chartData.map((_, index) => (index + 1).toString())
|
|
} else if (props.period === 'year') {
|
|
chartData = [...props.data]
|
|
.filter((item) => item && item.date)
|
|
.sort((a, b) => new Date(a.date) - new Date(b.date))
|
|
xAxisLabels = chartData.map((item) => {
|
|
const date = new Date(item.date)
|
|
return `${date.getMonth() + 1}月`
|
|
})
|
|
}
|
|
|
|
const expenseData = chartData.map((item) => {
|
|
const amount = item.amount || 0
|
|
return amount < 0 ? Math.abs(amount) : 0
|
|
})
|
|
const incomeData = chartData.map((item) => {
|
|
const amount = item.amount || 0
|
|
return amount > 0 ? amount : 0
|
|
})
|
|
|
|
return { chartData, xAxisLabels, expenseData, incomeData }
|
|
}
|
|
|
|
// Chart.js 数据
|
|
const chartData = computed(() => {
|
|
const { xAxisLabels, expenseData, incomeData } = prepareChartData()
|
|
|
|
return {
|
|
labels: xAxisLabels,
|
|
datasets: [
|
|
{
|
|
label: '支出',
|
|
data: expenseData,
|
|
borderColor: '#ff6b6b',
|
|
yAxisID: 'y',
|
|
order: 2,
|
|
backgroundColor: (context) => {
|
|
const chart = context.chart
|
|
const { ctx, chartArea } = chart
|
|
if (!chartArea) {return 'rgba(255, 107, 107, 0.1)'}
|
|
return createGradient(ctx, chartArea, '#ff6b6b')
|
|
},
|
|
fill: true,
|
|
tension: 0.4,
|
|
pointRadius: 0,
|
|
pointHoverRadius: 4,
|
|
borderWidth: 2
|
|
},
|
|
{
|
|
label: '收入',
|
|
data: incomeData,
|
|
borderColor: '#4ade80',
|
|
yAxisID: 'y',
|
|
order: 1,
|
|
backgroundColor: (context) => {
|
|
const chart = context.chart
|
|
const { ctx, chartArea } = chart
|
|
if (!chartArea) {return 'rgba(74, 222, 128, 0.1)'}
|
|
return createGradient(ctx, chartArea, '#4ade80')
|
|
},
|
|
fill: false,
|
|
tension: 0.4,
|
|
pointRadius: 0,
|
|
pointHoverRadius: 4,
|
|
borderWidth: 2
|
|
}
|
|
]
|
|
}
|
|
})
|
|
|
|
// Chart.js 配置
|
|
const chartOptions = computed(() => {
|
|
const { chartData: rawData, expenseData, incomeData } = prepareChartData()
|
|
const maxExpense = Math.max(...expenseData, 0)
|
|
const maxIncome = Math.max(...incomeData, 0)
|
|
const maxValue = Math.max(maxExpense, maxIncome, 0)
|
|
|
|
return getChartOptions({
|
|
layout: {
|
|
padding: {
|
|
bottom: 6
|
|
}
|
|
},
|
|
scales: {
|
|
x: {
|
|
display: false,
|
|
grid: { display: false, drawBorder: false },
|
|
ticks: { display: false },
|
|
border: { display: false }
|
|
},
|
|
y: {
|
|
display: false,
|
|
grid: { display: false, drawBorder: false },
|
|
ticks: { display: false },
|
|
border: { display: false },
|
|
beginAtZero: true,
|
|
suggestedMax: maxValue ? maxValue * 1.1 : undefined,
|
|
grace: '6%'
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: { display: false },
|
|
tooltip: {
|
|
callbacks: {
|
|
title: (context) => {
|
|
const index = context[0].dataIndex
|
|
if (!rawData[index]) {return ''}
|
|
|
|
const date = rawData[index].date
|
|
if (props.period === 'week') {
|
|
const dateObj = new Date(date)
|
|
const month = dateObj.getMonth() + 1
|
|
const day = dateObj.getDate()
|
|
const weekDays = ['日', '一', '二', '三', '四', '五', '六']
|
|
const weekDay = weekDays[dateObj.getDay()]
|
|
return `${month}月${day}日 (周${weekDay})`
|
|
} else if (props.period === 'month') {
|
|
const day = new Date(date).getDate()
|
|
return `${props.currentDate.getMonth() + 1}月${day}日`
|
|
} else if (props.period === 'year') {
|
|
const dateObj = new Date(date)
|
|
return `${dateObj.getFullYear()}年${dateObj.getMonth() + 1}月`
|
|
}
|
|
return ''
|
|
},
|
|
label: (context) => {
|
|
if (context.parsed.y === 0) {return null}
|
|
return `${context.dataset.label}: ¥${context.parsed.y.toFixed(1)}`
|
|
}
|
|
}
|
|
}
|
|
},
|
|
interaction: {
|
|
mode: 'index',
|
|
intersect: false
|
|
}
|
|
})
|
|
})
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
@import '@/assets/theme.css';
|
|
|
|
.daily-trend-card {
|
|
background: var(--bg-secondary);
|
|
border-radius: var(--radius-lg);
|
|
padding: var(--spacing-xl);
|
|
margin-bottom: var(--spacing-xl);
|
|
box-shadow: var(--shadow-sm);
|
|
}
|
|
|
|
.card-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: var(--spacing-lg);
|
|
}
|
|
|
|
.card-title {
|
|
font-size: var(--font-lg);
|
|
font-weight: var(--font-semibold);
|
|
color: var(--text-primary);
|
|
margin: 0;
|
|
}
|
|
|
|
.trend-chart {
|
|
width: 100%;
|
|
height: 200px;
|
|
}
|
|
</style>
|