1
This commit is contained in:
361
Web/src/views/statisticsV2/modules/DailyTrendChart.vue
Normal file
361
Web/src/views/statisticsV2/modules/DailyTrendChart.vue
Normal file
@@ -0,0 +1,361 @@
|
||||
<template>
|
||||
<div class="daily-trend-card common-card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">
|
||||
{{ chartTitle }} (收支)
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref="chartRef"
|
||||
class="trend-chart"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import { useMessageStore } from '@/stores/message'
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
period: {
|
||||
type: String,
|
||||
default: 'month'
|
||||
},
|
||||
currentDate: {
|
||||
type: Date,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const messageStore = useMessageStore()
|
||||
|
||||
const chartRef = ref()
|
||||
let chartInstance = null
|
||||
|
||||
// 计算图表标题
|
||||
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 initChart = async () => {
|
||||
await nextTick()
|
||||
|
||||
if (!chartRef.value) {
|
||||
console.warn('图表容器未找到')
|
||||
return
|
||||
}
|
||||
|
||||
// 销毁已存在的图表实例
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose()
|
||||
chartInstance = null
|
||||
}
|
||||
|
||||
try {
|
||||
chartInstance = echarts.init(chartRef.value)
|
||||
updateChart()
|
||||
} catch (error) {
|
||||
console.error('初始化图表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新图表
|
||||
const updateChart = () => {
|
||||
if (!chartInstance) {
|
||||
console.warn('图表实例不存在')
|
||||
return
|
||||
}
|
||||
|
||||
// 验证数据
|
||||
if (!Array.isArray(props.data)) {
|
||||
console.warn('图表数据格式错误')
|
||||
return
|
||||
}
|
||||
|
||||
// 根据时间段类型和数据来生成图表
|
||||
let chartData = []
|
||||
let xAxisLabels = []
|
||||
|
||||
try {
|
||||
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}月`
|
||||
})
|
||||
}
|
||||
|
||||
// 如果没有有效数据,显示空图表
|
||||
if (chartData.length === 0) {
|
||||
const option = {
|
||||
backgroundColor: 'transparent',
|
||||
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 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
|
||||
})
|
||||
|
||||
const isDark = messageStore.isDarkMode
|
||||
|
||||
const option = {
|
||||
backgroundColor: 'transparent',
|
||||
grid: {
|
||||
top: 20,
|
||||
left: 10,
|
||||
right: 10,
|
||||
bottom: 20,
|
||||
containLabel: false
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: xAxisLabels,
|
||||
show: false
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
show: false
|
||||
},
|
||||
series: [
|
||||
// 支出线
|
||||
{
|
||||
name: '支出',
|
||||
type: 'line',
|
||||
data: expenseData,
|
||||
smooth: true,
|
||||
symbol: 'none',
|
||||
lineStyle: {
|
||||
color: '#ff6b6b',
|
||||
width: 2
|
||||
},
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(255, 107, 107, 0.3)' },
|
||||
{ offset: 1, color: 'rgba(255, 107, 107, 0.05)' }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
// 收入线
|
||||
{
|
||||
name: '收入',
|
||||
type: 'line',
|
||||
data: incomeData,
|
||||
smooth: true,
|
||||
symbol: 'none',
|
||||
lineStyle: {
|
||||
color: '#4ade80',
|
||||
width: 2
|
||||
},
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [
|
||||
{ offset: 0, color: 'rgba(74, 222, 128, 0.3)' },
|
||||
{ offset: 1, color: 'rgba(74, 222, 128, 0.05)' }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: isDark ? 'rgba(39, 39, 42, 0.95)' : 'rgba(255, 255, 255, 0.95)',
|
||||
borderColor: isDark ? 'rgba(63, 63, 70, 0.8)' : 'rgba(229, 231, 235, 0.8)',
|
||||
textStyle: {
|
||||
color: isDark ? '#f4f4f5' : '#1a1a1a'
|
||||
},
|
||||
formatter: (params) => {
|
||||
if (!params || params.length === 0 || !chartData[params[0].dataIndex]) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const date = chartData[params[0].dataIndex].date
|
||||
let content = ''
|
||||
|
||||
try {
|
||||
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()]
|
||||
content = `${month}月${day}日 (周${weekDay})<br/>`
|
||||
} else if (props.period === 'month') {
|
||||
const day = new Date(date).getDate()
|
||||
content = `${props.currentDate.getMonth() + 1}月${day}日<br/>`
|
||||
} else if (props.period === 'year') {
|
||||
const dateObj = new Date(date)
|
||||
content = `${dateObj.getFullYear()}年${dateObj.getMonth() + 1}月<br/>`
|
||||
}
|
||||
|
||||
params.forEach((param) => {
|
||||
if (param.value > 0) {
|
||||
const color = param.seriesName === '支出' ? '#ff6b6b' : '#4ade80'
|
||||
content += `<span style="display:inline-block;margin-right:5px;border-radius:50%;width:10px;height:10px;background-color:${color}"></span>`
|
||||
content += `${param.seriesName}: ¥${param.value.toFixed(2)}<br/>`
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn('格式化tooltip失败:', error)
|
||||
content = '数据格式错误'
|
||||
}
|
||||
|
||||
return content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
chartInstance.setOption(option)
|
||||
} catch (error) {
|
||||
console.error('更新图表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 监听数据变化
|
||||
watch(() => props.data, () => {
|
||||
if (chartInstance) {
|
||||
updateChart()
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
// 监听主题变化
|
||||
watch(() => messageStore.isDarkMode, () => {
|
||||
if (chartInstance) {
|
||||
updateChart()
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
initChart()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/assets/theme.css';
|
||||
|
||||
.daily-trend-card {
|
||||
background: var(--bg-primary);
|
||||
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;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user