chore: migrate remaining ECharts components to Chart.js

- Migrated 4 components from ECharts to Chart.js:
  * MonthlyExpenseCard.vue (折线图)
  * DailyTrendChart.vue (双系列折线图)
  * ExpenseCategoryCard.vue (环形图)
  * BudgetChartAnalysis.vue (仪表盘 + 多种图表)

- Removed all ECharts imports and environment variable switches
- Unified all charts to use BaseChart.vue component
- Build verified: pnpm build success ✓
- No echarts imports remaining ✓

Refs: openspec/changes/migrate-remaining-echarts-to-chartjs
This commit is contained in:
SunCheng
2026-02-16 21:55:38 +08:00
parent a88556c784
commit 9921cd5fdf
77 changed files with 6964 additions and 1632 deletions

View File

@@ -6,17 +6,22 @@
</h3>
</div>
<div
ref="chartRef"
class="trend-chart"
/>
<div class="trend-chart">
<BaseChart
type="line"
:data="chartData"
:options="chartOptions"
:loading="false"
/>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'
import * as echarts from 'echarts'
import { useMessageStore } from '@/stores/message'
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: {
@@ -33,10 +38,8 @@ const props = defineProps({
}
})
const messageStore = useMessageStore()
const chartRef = ref()
let chartInstance = null
// Chart.js 相关
const { getChartOptions } = useChartTheme()
// 计算图表标题
const chartTitle = computed(() => {
@@ -57,284 +60,158 @@ 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
}
// 根据时间段类型和数据来生成图表
// 准备图表数据(通用)
const prepareChartData = () => {
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)
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 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'
}
}
]
const dataMap = new Map()
props.data.forEach((item) => {
if (item && item.date) {
dataMap.set(item.date, item)
}
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
chartData = allDays.map((date) => {
const dayData = dataMap.get(date)
return {
date,
amount: dayData?.amount || 0,
count: dayData?.count || 0
}
})
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)' }
]
}
}
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',
backgroundColor: (context) => {
const chart = context.chart
const { ctx, chartArea } = chart
if (!chartArea) {return 'rgba(255, 107, 107, 0.1)'}
return createGradient(ctx, chartArea, '#ff6b6b')
},
// 收入线
{
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)' }
]
}
}
}
],
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')
},
fill: true,
tension: 0.4,
pointRadius: 0,
pointHoverRadius: 4,
borderWidth: 2
}
]
}
})
// Chart.js 配置
const chartOptions = computed(() => {
const { chartData: rawData } = prepareChartData()
return getChartOptions({
scales: {
x: { display: false },
y: { display: false }
},
plugins: {
legend: { display: false },
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 ''
}
callbacks: {
title: (context) => {
const index = context[0].dataIndex
if (!rawData[index]) {return ''}
const date = chartData[params[0].dataIndex].date
let content = ''
try {
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()]
content = `${month}${day}日 (周${weekDay})<br/>`
return `${month}${day}日 (周${weekDay})`
} else if (props.period === 'month') {
const day = new Date(date).getDate()
content = `${props.currentDate.getMonth() + 1}${day}<br/>`
return `${props.currentDate.getMonth() + 1}${day}`
} else if (props.period === 'year') {
const dateObj = new Date(date)
content = `${dateObj.getFullYear()}${dateObj.getMonth() + 1}<br/>`
return `${dateObj.getFullYear()}${dateObj.getMonth() + 1}`
}
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 ''
},
label: (context) => {
if (context.parsed.y === 0) {return null}
return `${context.dataset.label}: ¥${context.parsed.y.toFixed(2)}`
}
return content
}
}
},
interaction: {
mode: 'index',
intersect: false
}
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>