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:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 = []
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user