refactor: 使用ucharts替换原生canvas绘制收益曲线

- 安装@qiun/ucharts依赖
- 使用qiun-ucharts组件绘制折线图
- 配置平滑曲线、渐变填充、网格线
- 移除原生canvas绘制代码,简化逻辑
- 支持触摸查看数据(ucharts内置)
This commit is contained in:
claw_bot 2026-03-14 09:41:07 +00:00
parent 2f30758d9a
commit fc05840e40
3 changed files with 68 additions and 207 deletions

7
package-lock.json generated
View File

@ -5,9 +5,16 @@
"packages": { "packages": {
"": { "": {
"dependencies": { "dependencies": {
"@qiun/ucharts": "^2.5.0-20230101",
"uview-plus": "^3.7.13" "uview-plus": "^3.7.13"
} }
}, },
"node_modules/@qiun/ucharts": {
"version": "2.5.0-20230101",
"resolved": "https://registry.npmjs.org/@qiun/ucharts/-/ucharts-2.5.0-20230101.tgz",
"integrity": "sha512-C7ccBgfPuGF6dxTRuMW0NPPMSCf1k/kh3I9zkRVBc5PaivudX/rPL+jd2Wty6gn5ya5L3Ob+YmYe09V5xw66Cw==",
"license": "Apache"
},
"node_modules/uview-plus": { "node_modules/uview-plus": {
"version": "3.7.13", "version": "3.7.13",
"resolved": "https://registry.npmjs.org/uview-plus/-/uview-plus-3.7.13.tgz", "resolved": "https://registry.npmjs.org/uview-plus/-/uview-plus-3.7.13.tgz",

View File

@ -1,5 +1,6 @@
{ {
"dependencies": { "dependencies": {
"@qiun/ucharts": "^2.5.0-20230101",
"uview-plus": "^3.7.13" "uview-plus": "^3.7.13"
} }
} }

View File

@ -81,22 +81,13 @@
<!-- 收益曲线 --> <!-- 收益曲线 -->
<view v-else class="nav-chart-wrapper"> <view v-else class="nav-chart-wrapper">
<canvas <qiun-ucharts
canvas-id="navChart" type="line"
id="navChart" :opts="chartOpts"
class="nav-canvas" :chartData="chartData"
@touchstart="onChartTouchStart" :canvas2d="true"
@touchmove="onChartTouchMove" canvasId="navChartCanvas"
></canvas> />
<!-- 悬浮提示 -->
<view v-if="showTooltip" class="chart-tooltip" :style="{left: tooltipX, top: tooltipY}">
<text class="tooltip-date">{{ tooltipDate }}</text>
<text class="tooltip-value">净值: {{ tooltipNav.toFixed(4) }}</text>
<text class="tooltip-return" :class="tooltipReturn >= 0 ? 'positive' : 'negative'">
收益: {{ tooltipReturn >= 0 ? '+' : '' }}{{ tooltipReturn.toFixed(2) }}%
</text>
</view>
</view> </view>
<!-- 统计指标 --> <!-- 统计指标 -->
@ -443,8 +434,9 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, getCurrentInstance } from 'vue'; import { ref, onMounted, getCurrentInstance, computed } from 'vue';
import { api } from '../../utils/api'; import { api } from '../../utils/api';
import qiunUcharts from '@qiun/ucharts/components/qiun-ucharts/qiun-ucharts.vue';
// u-toast // u-toast
const { proxy } = getCurrentInstance(); const { proxy } = getCurrentInstance();
@ -491,12 +483,57 @@ const navLoading = ref(false);
const navPeriod = ref('30d'); const navPeriod = ref('30d');
const navHistory = ref([]); const navHistory = ref([]);
const navStatistics = ref(null); const navStatistics = ref(null);
const showTooltip = ref(false);
const tooltipX = ref('0'); //
const tooltipY = ref('0'); const chartData = computed(() => {
const tooltipDate = ref(''); if (!navHistory.value || navHistory.value.length === 0) {
const tooltipNav = ref(0); return { categories: [], series: [] };
const tooltipReturn = ref(0); }
return {
categories: navHistory.value.map(item => item.date.split('-').slice(1).join('/')),
series: [{
name: '净值',
data: navHistory.value.map(item => item.nav)
}]
};
});
//
const chartOpts = {
color: ['#064E3B'],
padding: [15, 15, 0, 15],
enableScroll: false,
legend: {
show: false
},
xAxis: {
disableGrid: true,
axisLine: false,
fontSize: 10,
fontColor: '#6B7280'
},
yAxis: {
data: [{ min: 0 }],
gridColor: '#E5E7EB',
gridType: 'dash',
dashLength: 4,
fontSize: 10,
fontColor: '#6B7280'
},
extra: {
line: {
type: 'curve',
width: 2,
activeType: 'hollow'
},
area: {
type: 'curve',
opacity: 0.3,
addLine: true
}
}
};
// //
const showTransactionForm = ref(false); const showTransactionForm = ref(false);
@ -666,11 +703,6 @@ 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) {
setTimeout(() => drawNavChart(), 100);
}
} }
} catch (error) { } catch (error) {
console.error('获取净值历史失败:', error); console.error('获取净值历史失败:', error);
@ -685,143 +717,6 @@ const changeNavPeriod = (period) => {
fetchNavHistory(); fetchNavHistory();
}; };
// 线
const drawNavChart = () => {
const ctx = uni.createCanvasContext('navChart');
const data = navHistory.value;
if (!data || data.length === 0) return;
// canvasrpxpx
const width = 320; // 640rpx
const height = 200; // 400rpx
const padding = { top: 20, right: 20, bottom: 30, left: 50 };
const chartWidth = width - padding.left - padding.right;
const chartHeight = height - padding.top - padding.bottom;
//
const navValues = data.map(item => item.nav);
const minNav = Math.min(...navValues);
const maxNav = Math.max(...navValues);
const navRange = maxNav - minNav || 1;
//
ctx.clearRect(0, 0, width, height);
// 线
ctx.setStrokeStyle('#E5E7EB');
ctx.setLineWidth(0.5);
for (let i = 0; i <= 4; i++) {
const y = padding.top + (chartHeight / 4) * i;
ctx.beginPath();
ctx.moveTo(padding.left, y);
ctx.lineTo(width - padding.right, y);
ctx.stroke();
}
// Y
ctx.setFillStyle('#6B7280');
ctx.setFontSize(10);
ctx.setTextAlign('right');
for (let i = 0; i <= 4; i++) {
const y = padding.top + (chartHeight / 4) * i;
const value = maxNav - (navRange / 4) * i;
ctx.fillText(value.toFixed(2), padding.left - 5, y + 3);
}
// 线
ctx.beginPath();
ctx.setStrokeStyle('#064E3B');
ctx.setLineWidth(2);
data.forEach((item, index) => {
const x = padding.left + (chartWidth / (data.length - 1)) * index;
const y = padding.top + chartHeight - ((item.nav - minNav) / navRange) * chartHeight;
if (index === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
});
ctx.stroke();
//
ctx.beginPath();
data.forEach((item, index) => {
const x = padding.left + (chartWidth / (data.length - 1)) * index;
const y = padding.top + chartHeight - ((item.nav - minNav) / navRange) * chartHeight;
if (index === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
});
ctx.lineTo(padding.left + chartWidth, padding.top + chartHeight);
ctx.lineTo(padding.left, padding.top + chartHeight);
ctx.closePath();
const gradient = ctx.createLinearGradient(0, padding.top, 0, padding.top + chartHeight);
gradient.addColorStop(0, 'rgba(6, 78, 59, 0.3)');
gradient.addColorStop(1, 'rgba(6, 78, 59, 0.05)');
ctx.setFillStyle(gradient);
ctx.fill();
// X
ctx.setFillStyle('#6B7280');
ctx.setFontSize(10);
ctx.setTextAlign('center');
const labelCount = Math.min(5, data.length);
const labelStep = Math.floor(data.length / labelCount);
for (let i = 0; i < labelCount; i++) {
const index = i * labelStep;
if (index < data.length) {
const x = padding.left + (chartWidth / (data.length - 1)) * index;
const dateStr = data[index].date.split('-').slice(1).join('/');
ctx.fillText(dateStr, x, height - 10);
}
}
ctx.draw();
};
//
const onChartTouchStart = (e) => {
showChartTooltip(e);
};
const onChartTouchMove = (e) => {
showChartTooltip(e);
};
const showChartTooltip = (e) => {
if (!navHistory.value || navHistory.value.length === 0) return;
const touch = e.touches[0];
const data = navHistory.value;
//
const width = 320;
const padding = { left: 50, right: 20 };
const chartWidth = width - padding.left - padding.right;
const x = touch.x - padding.left;
const index = Math.round((x / chartWidth) * (data.length - 1));
if (index >= 0 && index < data.length) {
const item = data[index];
tooltipDate.value = item.date;
tooltipNav.value = item.nav;
tooltipReturn.value = item.cumulativeReturn;
// tooltip
tooltipX.value = `${Math.min(Math.max(touch.x - 50, 10), width - 110)}px`;
tooltipY.value = `${Math.max(touch.y - 80, 10)}px`;
showTooltip.value = true;
}
};
onMounted(async () => { onMounted(async () => {
loading.value = true; loading.value = true;
await fetchPortfolioData(); await fetchPortfolioData();
@ -1404,48 +1299,6 @@ const deletePortfolio = async () => {
height: 400rpx; height: 400rpx;
} }
.nav-canvas {
width: 640rpx;
height: 400rpx;
}
.chart-tooltip {
position: absolute;
background-color: rgba(0, 0, 0, 0.8);
border-radius: 12rpx;
padding: 16rpx 20rpx;
pointer-events: none;
z-index: 100;
}
.tooltip-date {
display: block;
font-size: 22rpx;
color: #FFFFFF;
margin-bottom: 8rpx;
}
.tooltip-value {
display: block;
font-size: 24rpx;
color: #FFFFFF;
margin-bottom: 4rpx;
}
.tooltip-return {
display: block;
font-size: 24rpx;
font-weight: 600;
}
.tooltip-return.positive {
color: #EF4444;
}
.tooltip-return.negative {
color: #10B981;
}
.period-tabs { .period-tabs {
display: flex; display: flex;
gap: 16rpx; gap: 16rpx;