refactor: 使用ucharts替换原生canvas绘制收益曲线
- 安装@qiun/ucharts依赖 - 使用qiun-ucharts组件绘制折线图 - 配置平滑曲线、渐变填充、网格线 - 移除原生canvas绘制代码,简化逻辑 - 支持触摸查看数据(ucharts内置)
This commit is contained in:
parent
2f30758d9a
commit
fc05840e40
7
package-lock.json
generated
7
package-lock.json
generated
@ -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",
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@qiun/ucharts": "^2.5.0-20230101",
|
||||||
"uview-plus": "^3.7.13"
|
"uview-plus": "^3.7.13"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
|
||||||
|
|
||||||
// 获取canvas尺寸(rpx转px)
|
|
||||||
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;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user