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