fix: 修复统计图表不显示与对比上月Infinity问题

- ChartWrapper: 添加 type 属性修复 uCharts 不渲染
- ChartWrapper: 使用 pixelRatio=1 避免双重缩放导致字体线条过大
- ChartWrapper: 统一 fontSize/fontColor/padding 配置
- ChartWrapper: 挂载时保存实例引用避免异步回调中 getCurrentInstance 返回 undefined
- stats: Number() 强制类型转换避免字符串导致 Infinity
- stats: !prev 宽松判断覆盖 null/undefined/0 除零情况
- stores: fetchOverview/fetchCategoryStats/fetchTrend 确保数据字段为数字
This commit is contained in:
2026-06-03 11:30:41 +08:00
parent db81ad8eb2
commit 9162b2c87c
3 changed files with 117 additions and 41 deletions

View File

@@ -1,6 +1,7 @@
<template>
<view class="chart-wrap">
<canvas
type="2d"
:canvas-id="canvasId"
:id="canvasId"
class="chart-canvas"
@@ -25,6 +26,8 @@ const props = withDefaults(defineProps<{
const emit = defineEmits(['tap'])
// 挂载时保存实例引用,避免异步回调中 getCurrentInstance 返回 undefined
const instance = getCurrentInstance()
let chartInstance: any = null
function drawChart() {
@@ -39,6 +42,43 @@ function drawChart() {
// #endif
}
function buildConfig(ctx: any, canvas2d: boolean, cssWidth: number, cssHeight: number) {
// canvas 已通过 ctx.scale(dpr) 处理缩放uCharts 用 pixelRatio=1 避免双重缩放
const base: any = {
type: props.type,
context: ctx,
canvas2d,
pixelRatio: 1,
width: cssWidth,
height: cssHeight,
animation: true,
background: '#FFFFFF',
padding: [20, 20, 20, 20],
fontSize: 10,
fontColor: '#8B7E7E',
title: { show: false },
subtitle: { show: false },
legend: { show: false },
dataLabel: false,
enableScroll: false,
...props.chartData
}
// 饼图开启数据标签
if (props.type === 'pie') {
base.dataLabel = true
base.padding = [30, 30, 30, 30]
}
// 折线图/柱状图坐标轴字体
if (props.type === 'line' || props.type === 'column') {
base.xAxis = { fontSize: 10, ...base.xAxis }
base.yAxis = { fontSize: 10, ...base.yAxis }
}
return base
}
function drawChartH5() {
try {
const canvas = document.querySelector(`#${props.canvasId}`) as HTMLCanvasElement
@@ -53,19 +93,7 @@ function drawChartH5() {
canvas.height = rect.height * dpr
ctx.scale(dpr, dpr)
const config: any = {
context: ctx,
canvas2d: true,
pixelRatio: dpr,
width: rect.width,
height: rect.height,
animation: true,
background: '#FFFFFF',
padding: [15, 15, 15, 15],
...props.chartData
}
chartInstance = new uCharts(config)
chartInstance = new uCharts(buildConfig(ctx, true, rect.width, rect.height))
} catch (e) {
console.error('[Chart] H5 draw error:', e)
}
@@ -73,11 +101,18 @@ function drawChartH5() {
function drawChartMP() {
try {
const query = uni.createSelectorQuery().in(getCurrentInstance()?.proxy)
if (!instance?.proxy) return
const query = uni.createSelectorQuery().in(instance.proxy)
query.select(`#${props.canvasId}`)
// @ts-ignore
.fields({ node: true, size: true })
.exec((res) => {
if (!res || !res[0]) return
.exec((res: any) => {
if (!res || !res[0] || !res[0].node) {
console.warn('[Chart] Canvas node not found, fallback to old API')
drawChartMPLegacy()
return
}
const canvas = res[0].node
const ctx = canvas.getContext('2d')
@@ -87,22 +122,29 @@ function drawChartMP() {
canvas.height = res[0].height * dpr
ctx.scale(dpr, dpr)
const config: any = {
context: ctx,
canvas2d: true,
pixelRatio: dpr,
width: res[0].width,
height: res[0].height,
animation: true,
background: '#FFFFFF',
padding: [15, 15, 15, 15],
...props.chartData
}
chartInstance = new uCharts(config)
chartInstance = new uCharts(buildConfig(ctx, true, res[0].width, res[0].height))
})
} catch (e) {
console.error('[Chart] MP draw error:', e)
drawChartMPLegacy()
}
}
// 兼容旧版小程序 canvas API
function drawChartMPLegacy() {
try {
const ctx = uni.createCanvasContext(props.canvasId, instance?.proxy)
if (!ctx) return
const query = uni.createSelectorQuery().in(instance?.proxy)
query.select(`#${props.canvasId}`)
.boundingClientRect((rect: any) => {
if (!rect) return
chartInstance = new uCharts(buildConfig(ctx, false, rect.width, rect.height))
})
.exec()
} catch (e) {
console.error('[Chart] MP legacy draw error:', e)
}
}

View File

@@ -57,7 +57,7 @@
canvasId="categoryChart"
type="pie"
:chartData="categoryChartData"
:width="600"
:width="650"
:height="400"
/>
<view class="chart-legend">
@@ -90,7 +90,7 @@
canvasId="trendChart"
type="line"
:chartData="trendChartData"
:width="600"
:width="650"
:height="400"
/>
<view class="trend-list">
@@ -149,9 +149,9 @@ const categoryChartData = computed(() => {
series: [{
data: categoryStats.value.map(item => ({
name: item.name,
value: item.amount / 100,
color: item.color
}))
value: item.amount / 100
})),
color: categoryStats.value.map(item => item.color)
}],
extra: {
pie: {
@@ -174,9 +174,9 @@ const trendChartData = computed(() => {
categories: trendData.value.map(p => p.date.slice(8) + '日'),
series: [{
name: tabType.value === 'expense' ? '支出' : '收入',
data: trendData.value.map(p => p.amount / 100),
color: '#FF8C69'
data: trendData.value.map(p => p.amount / 100)
}],
color: ['#FF8C69'],
extra: {
line: {
type: 'curve',
@@ -187,6 +187,15 @@ const trendChartData = computed(() => {
opacity: 0.2,
gradient: true
}
},
xAxis: {
disableGrid: true,
rotateLabel: true,
rotateAngle: -45
},
yAxis: {
gridColor: '#F0E0D6',
data: [{ min: 0 }]
}
}
})
@@ -204,7 +213,8 @@ const monthCompareText = computed(() => {
if (!prevMonthData.value) return ''
const current = tabType.value === 'expense' ? overview.value.expense : overview.value.income
const prev = tabType.value === 'expense' ? prevMonthData.value.expense : prevMonthData.value.income
if (prev === 0) return '无上月数据'
// 处理 prev 为 0、null、undefined 的情况,避免除零产生 Infinity
if (!prev) return '无上月数据'
const pct = Math.round(((current - prev) / prev) * 100)
if (pct > 0) return `+${pct}%`
if (pct < 0) return `${pct}%`
@@ -275,7 +285,13 @@ async function fetchPrevMonthData() {
const [y, m] = currentMonth.value.split('-').map(Number)
const prevMonth = m === 1 ? `${y - 1}-12` : `${y}-${String(m - 1).padStart(2, '0')}`
const data = await getOverview({ month: prevMonth })
prevMonthData.value = data
// 确保数据字段是数字类型,避免 null/undefined 导致 Infinity
prevMonthData.value = {
expense: Number(data?.expense) || 0,
income: Number(data?.income) || 0,
count: Number(data?.count) || 0,
daily: Number(data?.daily) || 0
}
} catch {
prevMonthData.value = null
}

View File

@@ -15,7 +15,14 @@ export const useStatsStore = defineStore('stats', () => {
async function fetchOverview(month?: string) {
try {
overview.value = await api.getOverview({ month: month || getCurrentMonth() })
const data = await api.getOverview({ month: month || getCurrentMonth() })
// 确保数据字段是数字类型
overview.value = {
expense: Number(data?.expense) || 0,
income: Number(data?.income) || 0,
count: Number(data?.count) || 0,
daily: Number(data?.daily) || 0
}
} catch {
overview.value = { expense: 0, income: 0, count: 0, daily: 0 }
}
@@ -23,7 +30,13 @@ export const useStatsStore = defineStore('stats', () => {
async function fetchCategoryStats(month?: string, type: string = 'expense') {
try {
categoryStats.value = await api.getCategoryStats(month || getCurrentMonth(), type)
const data = await api.getCategoryStats(month || getCurrentMonth(), type)
// 确保返回的是数组,且金额是数字
categoryStats.value = Array.isArray(data) ? data.map(item => ({
...item,
amount: Number(item.amount) || 0,
count: Number(item.count) || 0
})) : []
} catch {
categoryStats.value = []
}
@@ -31,7 +44,12 @@ export const useStatsStore = defineStore('stats', () => {
async function fetchTrend(month?: string, type: string = 'expense') {
try {
trendData.value = await api.getTrend(month || getCurrentMonth(), type)
const data = await api.getTrend(month || getCurrentMonth(), type)
// 确保返回的是数组,且金额是数字
trendData.value = Array.isArray(data) ? data.map(item => ({
...item,
amount: Number(item.amount) || 0
})) : []
} catch {
trendData.value = []
}