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> <template>
<view class="chart-wrap"> <view class="chart-wrap">
<canvas <canvas
type="2d"
:canvas-id="canvasId" :canvas-id="canvasId"
:id="canvasId" :id="canvasId"
class="chart-canvas" class="chart-canvas"
@@ -25,6 +26,8 @@ const props = withDefaults(defineProps<{
const emit = defineEmits(['tap']) const emit = defineEmits(['tap'])
// 挂载时保存实例引用,避免异步回调中 getCurrentInstance 返回 undefined
const instance = getCurrentInstance()
let chartInstance: any = null let chartInstance: any = null
function drawChart() { function drawChart() {
@@ -39,6 +42,43 @@ function drawChart() {
// #endif // #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() { function drawChartH5() {
try { try {
const canvas = document.querySelector(`#${props.canvasId}`) as HTMLCanvasElement const canvas = document.querySelector(`#${props.canvasId}`) as HTMLCanvasElement
@@ -53,19 +93,7 @@ function drawChartH5() {
canvas.height = rect.height * dpr canvas.height = rect.height * dpr
ctx.scale(dpr, dpr) ctx.scale(dpr, dpr)
const config: any = { chartInstance = new uCharts(buildConfig(ctx, true, rect.width, rect.height))
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)
} catch (e) { } catch (e) {
console.error('[Chart] H5 draw error:', e) console.error('[Chart] H5 draw error:', e)
} }
@@ -73,11 +101,18 @@ function drawChartH5() {
function drawChartMP() { function drawChartMP() {
try { try {
const query = uni.createSelectorQuery().in(getCurrentInstance()?.proxy) if (!instance?.proxy) return
const query = uni.createSelectorQuery().in(instance.proxy)
query.select(`#${props.canvasId}`) query.select(`#${props.canvasId}`)
// @ts-ignore
.fields({ node: true, size: true }) .fields({ node: true, size: true })
.exec((res) => { .exec((res: any) => {
if (!res || !res[0]) return 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 canvas = res[0].node
const ctx = canvas.getContext('2d') const ctx = canvas.getContext('2d')
@@ -87,22 +122,29 @@ function drawChartMP() {
canvas.height = res[0].height * dpr canvas.height = res[0].height * dpr
ctx.scale(dpr, dpr) ctx.scale(dpr, dpr)
const config: any = { chartInstance = new uCharts(buildConfig(ctx, true, res[0].width, res[0].height))
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)
}) })
} catch (e) { } catch (e) {
console.error('[Chart] MP draw error:', 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" canvasId="categoryChart"
type="pie" type="pie"
:chartData="categoryChartData" :chartData="categoryChartData"
:width="600" :width="650"
:height="400" :height="400"
/> />
<view class="chart-legend"> <view class="chart-legend">
@@ -90,7 +90,7 @@
canvasId="trendChart" canvasId="trendChart"
type="line" type="line"
:chartData="trendChartData" :chartData="trendChartData"
:width="600" :width="650"
:height="400" :height="400"
/> />
<view class="trend-list"> <view class="trend-list">
@@ -149,9 +149,9 @@ const categoryChartData = computed(() => {
series: [{ series: [{
data: categoryStats.value.map(item => ({ data: categoryStats.value.map(item => ({
name: item.name, name: item.name,
value: item.amount / 100, value: item.amount / 100
color: item.color })),
})) color: categoryStats.value.map(item => item.color)
}], }],
extra: { extra: {
pie: { pie: {
@@ -174,9 +174,9 @@ const trendChartData = computed(() => {
categories: trendData.value.map(p => p.date.slice(8) + '日'), categories: trendData.value.map(p => p.date.slice(8) + '日'),
series: [{ series: [{
name: tabType.value === 'expense' ? '支出' : '收入', name: tabType.value === 'expense' ? '支出' : '收入',
data: trendData.value.map(p => p.amount / 100), data: trendData.value.map(p => p.amount / 100)
color: '#FF8C69'
}], }],
color: ['#FF8C69'],
extra: { extra: {
line: { line: {
type: 'curve', type: 'curve',
@@ -187,6 +187,15 @@ const trendChartData = computed(() => {
opacity: 0.2, opacity: 0.2,
gradient: true 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 '' if (!prevMonthData.value) return ''
const current = tabType.value === 'expense' ? overview.value.expense : overview.value.income const current = tabType.value === 'expense' ? overview.value.expense : overview.value.income
const prev = tabType.value === 'expense' ? prevMonthData.value.expense : prevMonthData.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) const pct = Math.round(((current - prev) / prev) * 100)
if (pct > 0) return `+${pct}%` if (pct > 0) return `+${pct}%`
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 [y, m] = currentMonth.value.split('-').map(Number)
const prevMonth = m === 1 ? `${y - 1}-12` : `${y}-${String(m - 1).padStart(2, '0')}` const prevMonth = m === 1 ? `${y - 1}-12` : `${y}-${String(m - 1).padStart(2, '0')}`
const data = await getOverview({ month: prevMonth }) 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 { } catch {
prevMonthData.value = null prevMonthData.value = null
} }

View File

@@ -15,7 +15,14 @@ export const useStatsStore = defineStore('stats', () => {
async function fetchOverview(month?: string) { async function fetchOverview(month?: string) {
try { 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 { } catch {
overview.value = { expense: 0, income: 0, count: 0, daily: 0 } 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') { async function fetchCategoryStats(month?: string, type: string = 'expense') {
try { 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 { } catch {
categoryStats.value = [] categoryStats.value = []
} }
@@ -31,7 +44,12 @@ export const useStatsStore = defineStore('stats', () => {
async function fetchTrend(month?: string, type: string = 'expense') { async function fetchTrend(month?: string, type: string = 'expense') {
try { 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 { } catch {
trendData.value = [] trendData.value = []
} }