topfans/frontend/pages/dashboard/components/IncomeCurve.vue
2026-06-03 01:20:51 +08:00

250 lines
6.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="income-curve-card">
<text class="curve-title">七日收益曲线</text>
<!-- 错误态 -->
<view v-if="error" class="error-box" @tap="$emit('retry')">
<text class="error-text">加载失败,点击重试</text>
</view>
<!-- 骨架态 -->
<view v-else-if="loading || !points || points.length === 0" class="skeleton-chart"></view>
<!-- 图表 -->
<view v-else class="chart-wrap">
<view class="chart-canvas">
<qiun-data-charts
type="area"
:opts="chartOpts"
:chartData="chartData"
:ontouch="true"
:onmovetip="true"
canvas2d
ontap
@complete="handleChartComplete"
/>
<!-- 默认指向最后一天的指示器(位置来自 uCharts calPoints跟图表完全对齐 -->
<view
v-if="latestPoint && latestCalPoint"
class="latest-indicator"
:style="{
left: latestCalPoint.x / pixel + 'px',
top: latestCalPoint.y / pixel + 'px',
}"
>
<view class="latest-line"></view>
<view class="latest-dot"></view>
<view class="latest-tooltip">
<text class="latest-value">+{{ latestPoint.income }}</text>
<text class="latest-date">{{ latestPoint.date.slice(5) }}</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { computed, ref } from 'vue'
// 显式 import 兜底,避免 easycom 漏注册
import QiunDataCharts from '@/uni_modules/qiun-data-charts/components/qiun-data-charts/qiun-data-charts.vue'
const props = defineProps({
points: { type: Array, default: () => [] },
loading: { type: Boolean, default: false },
error: { type: String, default: null },
})
defineEmits(['retry'])
// canvas2d 模式 opts.pix=1非 canvas2d 模式 opts.pix=systemInfo.pixelRatio
// 用 emit 回来的 opts 拿真实值,兜底 1
const pixel = ref(1)
const chartOptsRef = ref(null)
const calPoints = ref([])
// 监听 @completeuCharts 渲染完成后会 emit 一次opts.chartData.calPoints 是每个点的真实 canvas 坐标
function handleChartComplete(e) {
const opts = e?.opts
if (!opts) return
chartOptsRef.value = opts
pixel.value = opts.pix || 1
calPoints.value = opts.chartData?.calPoints || []
}
// 跟 chartOpts.padding / chart-canvas 高度保持一致,单位 rpx
const CHART_HEIGHT = 240
const PADDING = [16, 16, 8, 16] // top, right, bottom, left
const peak = computed(() => props.points.find((p) => p.is_peak) || null)
const latestPoint = computed(() =>
props.points.length > 0 ? props.points[props.points.length - 1] : null
)
// 最后一点的真实 canvas 坐标(来自 uCharts @complete 事件的 calPoints
// calPoints 结构calPoints[seriesIndex][dataIndex] = { x, y }y 是 canvas 像素
const latestCalPoint = computed(() => {
if (!latestPoint.value || calPoints.value.length === 0) return null
const seriesPoints = calPoints.value[0]
if (!seriesPoints || seriesPoints.length === 0) return null
return seriesPoints[seriesPoints.length - 1] || null
})
const chartData = computed(() => {
const categories = props.points.map((p) => p.date.slice(5))
const lineData = props.points.map((p) => p.income)
return {
categories,
series: [
{ name: '收益', type: 'area', data: lineData, color: '#1BAFEE' },
],
}
})
const chartOpts = {
color: ['#1BAFEE'],
padding: PADDING,
dataLabel: false,
legend: { show: false },
// 关闭曲线上每个数据点的小圆圈
dataPointShape: false,
xAxis: { disabled: true, disableGrid: true, axisLine: false, fontColor: '#FFFFFF', fontSize: 9 },
yAxis: {
disabled: true,
showTitle: false,
data: [{ min: 0, disabled: true, axisLine: false }],
disableGrid: true,
fontColor: '#FFFFFF',
fontSize: 9,
},
extra: {
tooltip: {
showBox: true,
showArrow: true,
showCategory: true,
bgColor: '#000000',
bgOpacity: 0.6,
fontColor: '#FFFFFF',
fontSize: 11,
splitLine: true,
horizentalLine: { type: 'dash', width: 1, color: '#FFFFFF' },
},
// area 类型曲线区域图gradient: true 启用从线色到透明的渐变填充
area: { type: 'curve', opacity: 0.3, addLine: true, width: 2, gradient: true, activeType: 'hollow' },
},
}
</script>
<style lang="scss" scoped>
.income-curve-card {
background: linear-gradient(135deg, #FFDF77 0%, #8E95E2 40%, #F48CFF 100%);
border-radius: 22rpx;
padding: 24rpx;
margin: 24rpx 0;
box-shadow: 0px 4px 4px rgba(189, 50, 50, 0.25);
min-height: 360rpx;
}
.curve-title {
display: block;
font-size: 28rpx;
font-weight: 600;
color: #ffffff;
margin-bottom: 16rpx;
text-shadow: 0px 2px 2px rgba(0, 0, 0, 0.2);
}
.chart-wrap {
background: rgba(255, 255, 255, 0.15);
border-radius: 17rpx;
padding: 16rpx;
backdrop-filter: blur(10px);
}
.chart-canvas {
position: relative;
width: 100%;
height: 240rpx;
}
.latest-indicator {
position: absolute;
z-index: 2;
// left/top 是数据点的中心translate(-50%, -100%) 让 tooltip 在点的正上方、点对齐 indicator 中心
transform: translate(-50%, -100%);
display: flex;
flex-direction: column;
align-items: center;
pointer-events: none;
}
.latest-line {
width: 1rpx;
height: 60rpx;
background: rgba(255, 255, 255, 0.7);
margin-bottom: -2rpx;
align-self: center;
}
.latest-dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background: #1BAFEE;
border: 2rpx solid #ffffff;
align-self: center;
margin-bottom: 6rpx;
}
.latest-tooltip {
background: rgba(0, 0, 0, 0.6);
border-radius: 8rpx;
padding: 6rpx 12rpx;
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 4rpx;
}
.latest-value {
font-size: 24rpx;
font-weight: 700;
color: #FFFABD;
line-height: 1.2;
}
.latest-date {
font-size: 18rpx;
color: rgba(255, 255, 255, 0.85);
line-height: 1.2;
margin-top: 2rpx;
}
.skeleton-chart {
height: 360rpx;
border-radius: 17rpx;
background: linear-gradient(90deg, rgba(255, 255, 255, 0.1) 25%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.1) 75%);
background-size: 200% 100%;
animation: skeleton-shimmer 1.5s infinite;
}
.error-box {
height: 360rpx;
border-radius: 17rpx;
background: rgba(255, 100, 100, 0.15);
border: 2rpx solid rgba(255, 100, 100, 0.4);
display: flex;
align-items: center;
justify-content: center;
}
.error-text {
color: #ff8080;
font-size: 28rpx;
}
@keyframes skeleton-shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
</style>