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

@@ -21,9 +21,12 @@
class="chart-container"
>
<div class="ring-chart">
<div
ref="pieChartRef"
style="width: 100%; height: 100%"
<BaseChart
type="doughnut"
:data="chartData"
:options="chartOptions"
:loading="false"
@chart:render="onChartRender"
/>
</div>
</div>
@@ -79,10 +82,11 @@
</template>
<script setup>
import { ref, computed, onBeforeUnmount, nextTick, watch } from 'vue'
import * as echarts from 'echarts'
import { ref, computed } from 'vue'
import { getCssVar } from '@/utils/theme'
import ModernEmpty from '@/components/ModernEmpty.vue'
import BaseChart from '@/components/Charts/BaseChart.vue'
import { useChartTheme } from '@/composables/useChartTheme'
const props = defineProps({
categories: {
@@ -101,10 +105,12 @@ const props = defineProps({
defineEmits(['category-click'])
const pieChartRef = ref(null)
let pieChartInstance = null
const showAllExpense = ref(false)
// Chart.js 相关
const { getChartOptions } = useChartTheme()
let _chartJSInstance = null
// 格式化金额
const formatMoney = (value) => {
if (!value && value !== 0) {
@@ -133,7 +139,6 @@ const expenseCategoriesSimpView = computed(() => {
return list
}
// 只展示未分类
const unclassified = list.filter((c) => c.classify === '未分类' || !c.classify)
if (unclassified.length > 0) {
return [...unclassified]
@@ -141,142 +146,94 @@ const expenseCategoriesSimpView = computed(() => {
return []
})
// 渲染饼图
const renderPieChart = () => {
if (!pieChartRef.value) {
return
}
if (expenseCategoriesView.value.length === 0) {
return
}
// 尝试获取DOM上的现有实例
const existingInstance = echarts.getInstanceByDom(pieChartRef.value)
if (pieChartInstance && pieChartInstance !== existingInstance) {
if (!pieChartInstance.isDisposed()) {
pieChartInstance.dispose()
}
pieChartInstance = null
}
if (pieChartInstance && pieChartInstance.getDom() !== pieChartRef.value) {
pieChartInstance.dispose()
pieChartInstance = null
}
if (!pieChartInstance && existingInstance) {
pieChartInstance = existingInstance
}
if (!pieChartInstance) {
pieChartInstance = echarts.init(pieChartRef.value)
}
// 使用 Top N + Other 的数据逻辑,确保图表不会太拥挤
// 准备图表数据(通用)
const prepareChartData = () => {
const list = [...expenseCategoriesView.value]
let chartData = []
// 按照金额排序
list.sort((a, b) => b.amount - a.amount)
const MAX_SLICES = 8 // 最大显示扇区数,其余合并为"其他"
const MAX_SLICES = 8
if (list.length > MAX_SLICES) {
const topList = list.slice(0, MAX_SLICES - 1)
const otherList = list.slice(MAX_SLICES - 1)
const otherAmount = otherList.reduce((sum, item) => sum + item.amount, 0)
chartData = topList.map((item, index) => ({
const chartData = topList.map((item, index) => ({
label: item.classify || '未分类',
value: item.amount,
name: item.classify || '未分类',
itemStyle: { color: props.colors[index % props.colors.length] }
color: props.colors[index % props.colors.length]
}))
chartData.push({
label: '其他',
value: otherAmount,
name: '其他',
itemStyle: { color: getCssVar('--van-gray-6') } // 使用灰色表示其他
color: getCssVar('--van-gray-6')
})
return chartData
} else {
chartData = list.map((item, index) => ({
return list.map((item, index) => ({
label: item.classify || '未分类',
value: item.amount,
name: item.classify || '未分类',
itemStyle: { color: props.colors[index % props.colors.length] }
color: props.colors[index % props.colors.length]
}))
}
}
const option = {
title: {
text: '¥' + formatMoney(props.totalExpense),
subtext: '总支出',
left: 'center',
top: 'center',
textStyle: {
color: getCssVar('--chart-text-muted'), // 适配深色模式
fontSize: 20,
fontWeight: 'bold'
},
subtextStyle: {
color: getCssVar('--chart-text-muted'),
fontSize: 13
}
},
tooltip: {
trigger: 'item',
formatter: (params) => {
return `${params.name}: ¥${formatMoney(params.value)} (${params.percent.toFixed(1)}%)`
}
},
series: [
// Chart.js 数据
const chartData = computed(() => {
const data = prepareChartData()
return {
labels: data.map((item) => item.label),
datasets: [
{
name: '支出分类',
type: 'pie',
radius: ['50%', '80%'],
avoidLabelOverlap: true,
minAngle: 5, // 最小扇区角度,防止扇区太小看不见
itemStyle: {
borderRadius: 5,
borderColor: getCssVar('--van-background-2'),
borderWidth: 2
},
label: {
show: false
},
labelLine: {
show: false
},
data: chartData
data: data.map((item) => item.value),
backgroundColor: data.map((item) => item.color),
borderWidth: 2,
borderColor: getCssVar('--van-background-2') || '#fff',
hoverOffset: 4
}
]
}
pieChartInstance.setOption(option)
}
// 监听数据变化重新渲染图表
watch(
() => [props.categories, props.totalExpense, props.colors],
() => {
nextTick(() => {
renderPieChart()
})
},
{ deep: true, immediate: true }
)
// 组件销毁时清理图表实例
onBeforeUnmount(() => {
if (pieChartInstance && !pieChartInstance.isDisposed()) {
pieChartInstance.dispose()
}
})
// Chart.js 配置
const chartOptions = computed(() => {
return getChartOptions({
cutout: '50%',
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: (context) => {
const label = context.label || ''
const value = context.parsed || 0
const total = context.dataset.data.reduce((a, b) => a + b, 0)
const percentage = total > 0 ? ((value / total) * 100).toFixed(1) : 0
return `${label}: ¥${formatMoney(value)} (${percentage}%)`
}
}
}
},
onClick: (_event, _elements) => {
// 点击饼图扇区时,触发跳转到分类详情
// 注意:这个功能在 BaseChart 中不会自动触发,需要后续完善
}
})
})
// Chart.js 渲染完成回调
const onChartRender = (chart) => {
_chartJSInstance = chart
}
</script>
<style scoped lang="scss">
@import '@/assets/theme.css';
// 通用卡片样式
.common-card {
background: var(--bg-secondary);
border-radius: var(--radius-lg);
@@ -299,7 +256,6 @@ onBeforeUnmount(() => {
margin: 0;
}
/* 环形图 */
.chart-container {
padding: 0;
}
@@ -311,7 +267,6 @@ onBeforeUnmount(() => {
margin: 0 auto;
}
/* 分类列表 */
.category-list {
padding: 0;
}