Files
EmailBill/Web/src/views/statisticsV2/modules/DailyTrendChart.vue

248 lines
6.2 KiB
Vue
Raw Normal View History

2026-02-11 13:00:01 +08:00
<template>
2026-02-09 19:25:51 +08:00
<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>
2026-02-09 19:25:51 +08:00
</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'
2026-02-09 19:25:51 +08:00
const props = defineProps({
data: {
type: Array,
default: () => []
},
period: {
type: String,
default: 'month'
},
currentDate: {
type: Date,
required: true
}
})
// Chart.js 相关
const { getChartOptions } = useChartTheme()
2026-02-09 19:25:51 +08:00
// 计算图表标题
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 = () => {
2026-02-09 19:25:51 +08:00
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}`
})
2026-02-09 19:25:51 +08:00
const dataMap = new Map()
props.data.forEach((item) => {
if (item && item.date) {
dataMap.set(item.date, item)
2026-02-09 19:25:51 +08:00
}
})
2026-02-09 19:25:51 +08:00
chartData = allDays.map((date) => {
const dayData = dataMap.get(date)
return {
date,
amount: dayData?.amount || 0,
count: dayData?.count || 0
}
2026-02-09 19:25:51 +08:00
})
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}`
2026-02-09 19:25:51 +08:00
})
}
2026-02-09 19:25:51 +08:00
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
})
2026-02-09 19:25:51 +08:00
return { chartData, xAxisLabels, expenseData, incomeData }
}
// Chart.js 数据
const chartData = computed(() => {
const { xAxisLabels, expenseData, incomeData } = prepareChartData()
return {
labels: xAxisLabels,
datasets: [
{
label: '支出',
data: expenseData,
borderColor: '#ff6b6b',
backgroundColor: (context) => {
const chart = context.chart
const { ctx, chartArea } = chart
if (!chartArea) {return 'rgba(255, 107, 107, 0.1)'}
return createGradient(ctx, chartArea, '#ff6b6b')
2026-02-09 19:25:51 +08:00
},
fill: true,
tension: 0.4,
pointRadius: 0,
pointHoverRadius: 4,
borderWidth: 2
},
{
label: '收入',
data: incomeData,
borderColor: '#4ade80',
backgroundColor: (context) => {
const chart = context.chart
const { ctx, chartArea } = chart
if (!chartArea) {return 'rgba(74, 222, 128, 0.1)'}
return createGradient(ctx, chartArea, '#4ade80')
2026-02-09 19:25:51 +08:00
},
fill: true,
tension: 0.4,
pointRadius: 0,
pointHoverRadius: 4,
borderWidth: 2
}
]
}
})
2026-02-09 19:25:51 +08:00
// Chart.js 配置
const chartOptions = computed(() => {
const { chartData: rawData } = prepareChartData()
return getChartOptions({
scales: {
x: { display: false },
y: { display: false }
},
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
title: (context) => {
const index = context[0].dataIndex
if (!rawData[index]) {return ''}
2026-02-09 19:25:51 +08:00
const date = rawData[index].date
2026-02-09 19:25:51 +08:00
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})`
2026-02-09 19:25:51 +08:00
} else if (props.period === 'month') {
const day = new Date(date).getDate()
return `${props.currentDate.getMonth() + 1}${day}`
2026-02-09 19:25:51 +08:00
} else if (props.period === 'year') {
const dateObj = new Date(date)
return `${dateObj.getFullYear()}${dateObj.getMonth() + 1}`
2026-02-09 19:25:51 +08:00
}
return ''
},
label: (context) => {
if (context.parsed.y === 0) {return null}
return `${context.dataset.label}: ¥${context.parsed.y.toFixed(2)}`
2026-02-09 19:25:51 +08:00
}
}
}
},
interaction: {
mode: 'index',
intersect: false
2026-02-09 19:25:51 +08:00
}
})
2026-02-09 19:25:51 +08:00
})
</script>
<style scoped lang="scss">
@import '@/assets/theme.css';
.daily-trend-card {
2026-02-11 13:00:01 +08:00
background: var(--bg-secondary);
2026-02-09 19:25:51 +08:00
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: 180px;
}
2026-02-15 10:10:28 +08:00
</style>