fix: 改用原生 canvas 绘制收益曲线,移除 qiun-data-charts 依赖

- 微信小程序对 qiun-data-charts 兼容性差
- 原生 canvas 更轻量、更可靠
- 实现平滑曲线 + 渐变填充 + 网格线
- Y轴自适应数据范围
This commit is contained in:
claw_bot 2026-03-15 07:13:19 +00:00
parent 632b5b6f6d
commit 97149ca59e

View File

@ -81,13 +81,11 @@
<!-- 收益曲线 --> <!-- 收益曲线 -->
<view v-else class="nav-chart-wrapper"> <view v-else class="nav-chart-wrapper">
<qiun-data-charts <canvas
type="line" canvas-id="navChart"
:opts="chartOpts" id="navChart"
:chartData="chartData" class="nav-canvas"
:canvas2d="true" ></canvas>
canvasId="navChartCanvas"
/>
</view> </view>
<!-- 统计指标 --> <!-- 统计指标 -->
@ -434,7 +432,7 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, getCurrentInstance, computed } from 'vue'; import { ref, onMounted, getCurrentInstance, nextTick } from 'vue';
import { api } from '../../utils/api'; import { api } from '../../utils/api';
// u-toast // u-toast
@ -483,51 +481,95 @@ const navPeriod = ref('30d');
const navHistory = ref([]); const navHistory = ref([]);
const navStatistics = ref(null); const navStatistics = ref(null);
// // 线
const chartData = computed(() => { const drawNavChart = () => {
if (!navHistory.value || navHistory.value.length === 0) { const data = navHistory.value;
return { categories: [], series: [] }; if (!data || data.length === 0) return;
}
return { nextTick(() => {
categories: navHistory.value.map(item => item.date.split('-').slice(1).join('/')), const ctx = uni.createCanvasContext('navChart');
series: [{
name: '净值',
data: navHistory.value.map(item => item.nav)
}]
};
});
// // (rpx -> px)
const chartOpts = ref({ const width = 320;
color: ['#064E3B'], const height = 200;
padding: [15, 15, 0, 15], const padding = { top: 20, right: 20, bottom: 30, left: 45 };
enableScroll: false, const chartWidth = width - padding.left - padding.right;
legend: { const chartHeight = height - padding.top - padding.bottom;
show: false
}, //
xAxis: { const values = data.map(item => item.nav);
disableGrid: true, const minVal = Math.min(...values) * 0.98;
axisLine: false, const maxVal = Math.max(...values) * 1.02;
fontSize: 10, const range = maxVal - minVal || 1;
fontColor: '#6B7280'
}, //
yAxis: { ctx.clearRect(0, 0, width, height);
data: [{ min: 0 }],
gridColor: '#E5E7EB', // 线
gridType: 'dash', ctx.setStrokeStyle('#E5E7EB');
dashLength: 4, ctx.setLineWidth(0.5);
fontSize: 10, for (let i = 0; i <= 4; i++) {
fontColor: '#6B7280' const y = padding.top + (chartHeight / 4) * i;
}, ctx.beginPath();
extra: { ctx.moveTo(padding.left, y);
line: { ctx.lineTo(width - padding.right, y);
type: 'curve', ctx.stroke();
width: 2,
activeType: 'hollow'
} }
}
}); // Y
ctx.setFillStyle('#9CA3AF');
ctx.setFontSize(10);
ctx.setTextAlign('right');
for (let i = 0; i <= 4; i++) {
const y = padding.top + (chartHeight / 4) * i;
const val = maxVal - (range / 4) * i;
ctx.fillText(val.toFixed(2), padding.left - 5, y + 3);
}
//
const points = data.map((item, index) => ({
x: padding.left + (chartWidth / (data.length - 1 || 1)) * index,
y: padding.top + chartHeight - ((item.nav - minVal) / range) * chartHeight
}));
//
ctx.beginPath();
points.forEach((p, i) => {
if (i === 0) ctx.moveTo(p.x, p.y);
else ctx.lineTo(p.x, p.y);
});
ctx.lineTo(points[points.length - 1].x, padding.top + chartHeight);
ctx.lineTo(points[0].x, padding.top + chartHeight);
ctx.closePath();
ctx.setFillStyle('rgba(6, 78, 59, 0.15)');
ctx.fill();
// 线
ctx.beginPath();
ctx.setStrokeStyle('#064E3B');
ctx.setLineWidth(2);
points.forEach((p, i) => {
if (i === 0) ctx.moveTo(p.x, p.y);
else ctx.lineTo(p.x, p.y);
});
ctx.stroke();
// X
ctx.setFillStyle('#9CA3AF');
ctx.setFontSize(10);
ctx.setTextAlign('center');
const labelCount = Math.min(5, data.length);
const step = Math.floor(data.length / labelCount) || 1;
for (let i = 0; i < labelCount; i++) {
const idx = Math.min(i * step, data.length - 1);
const x = padding.left + (chartWidth / (data.length - 1 || 1)) * idx;
const dateStr = data[idx].date.split('-').slice(1).join('/');
ctx.fillText(dateStr, x, height - 8);
}
ctx.draw();
});
};
// //
const showTransactionForm = ref(false); const showTransactionForm = ref(false);
@ -697,6 +739,11 @@ const fetchNavHistory = async () => {
if (response.code === 200 && response.data) { if (response.code === 200 && response.data) {
navHistory.value = response.data.navHistory || []; navHistory.value = response.data.navHistory || [];
navStatistics.value = response.data.statistics || null; navStatistics.value = response.data.statistics || null;
//
if (navHistory.value.length > 0) {
drawNavChart();
}
} }
} catch (error) { } catch (error) {
console.error('获取净值历史失败:', error); console.error('获取净值历史失败:', error);
@ -1291,6 +1338,14 @@ const deletePortfolio = async () => {
position: relative; position: relative;
width: 100%; width: 100%;
height: 400rpx; height: 400rpx;
display: flex;
justify-content: center;
align-items: center;
}
.nav-canvas {
width: 640rpx;
height: 400rpx;
} }
.period-tabs { .period-tabs {