All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 4m27s
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
361 lines
8.9 KiB
Vue
361 lines
8.9 KiB
Vue
<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-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: 180px;
|
|
}
|
|
</style> |