topfans/frontend/pages/dashboard/components/IncomeCurve.vue

257 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>
<image
class="chart-bg"
src="/static/dashboard/ucharts-bj.png"
mode="scaleToFill"
/>
<!-- 错误态 -->
<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"
:in-scroll-view="true"
:tooltipShow="true"
:canvas2d="false"
canvasId="incomeCurveCanvas"
:canvasHeight="240"
ontap
@tap="onChartTap"
/>
</view>
</view>
</view>
</template>
<script setup>
import { computed, ref, watch } 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"]);
// 当前选中的数据点索引默认指向最后一条tap 时更新
const currentIndex = ref(0);
watch(
() => props.points.length,
(len) => {
currentIndex.value = Math.max(0, len - 1);
},
{ immediate: true },
);
const onChartTap = (e) => {
const idx = e?.index;
if (typeof idx === "number" && idx >= 0 && idx < props.points.length) {
currentIndex.value = idx;
}
};
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",
// [自定义] u-charts.js 已 patchseries.linearColor 启用横向多色渐变
// 对应 CSS: linear-gradient(90deg, #D1EFFFBC 0%, #E7E8A9D8 49.52%, #FF9CE5FA 100%)
linearColor: [
[0, "rgba(209, 239, 255, 0.735)"],
[0.4952, "rgba(231, 232, 169, 0.846)"],
[1, "rgba(255, 156, 229, 0.98)"],
],
linearDirection: "horizontal", // 'horizontal'(默认) | 'vertical'
},
],
};
});
const PADDING = [16, -16, 8, -16]; // top, right, bottom, left
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: false,
bgColor: "#000000",
bgOpacity: 0.6,
fontColor: "#FFFFFF",
fontSize: 11,
splitLine: true,
horizentalLine: { type: "dash", width: 1, color: "#FFFFFF" },
// 函数形式uCharts 每次渲染 tooltip 时调用index 为当前触点对应的数据点索引
// 未触摸时 index < 0返回空 tooltip 避免干扰
tooltipCustom: (_opts, _categories, index) => {
if (typeof index !== "number" || index < 0) {
return { textList: [] };
}
const point = props.points[index];
if (!point) return { textList: [] };
return {
textList: [
{ text: `+${point.income}`, color: "#1BAFEE" },
{ text: point.date.slice(5), color: "#999999" },
],
};
},
},
// area 类型曲线区域图,搭配 series.linearType=custom + linearColor 实现自定义多色渐变
// addLine:false 不绘制曲线(曲线透明),仅保留渐变面积填充
area: {
type: "curve",
opacity: 1,
addLine: false,
width: 2,
gradient: true,
activeType: "hollow",
},
},
};
</script>
<style lang="scss" scoped>
.income-curve-card {
background: linear-gradient(
107.96deg,
rgba(255, 223, 119, 0.59) -28.33%,
rgba(142, 149, 226, 0.59) 44.4%,
rgba(244, 140, 255, 0.59) 117.77%
);
border-radius: 22rpx;
padding: 24rpx;
box-sizing: border-box; // padding 计入总高度,避免内部撑爆
box-shadow: 0px 4px 4px 0px #BD323240;
position: relative;
height: 256rpx;
display: flex;
flex-direction: column;
overflow: hidden; // 裁掉超出圆角的部分(包括背景图)
}
.curve-title {
display: block;
font-size: 28rpx;
font-weight: 600;
color: #ffffff;
margin-bottom: 4rpx; // 卡片高度有限,缩小标题与图表间距
text-shadow: 0px 2px 2px rgba(0, 0, 0, 0.2);
position: relative;
z-index: 2; // 标题在背景图和图表之上
}
.chart-wrap {
position: relative;
flex: 1; // 撑满标题下方的剩余空间
min-height: 0; // flex 子项防止溢出的兜底
border-radius: 17rpx;
overflow: hidden;
z-index: 1;
// backdrop-filter: blur(10px);
}
.chart-bg {
position: absolute;
bottom: 0;
left: 0;
width: 224rpx;
height: 100%;
z-index: 0; // 在标题(z:2)和图表(z:1)之下
pointer-events: none; // 不拦截 tap事件穿透到 canvas
opacity: 0.4;
}
.chart-canvas {
position: relative;
width: 100%;
height: 100%; // 跟随 .chart-wrap 撑满
// 阴影drop-shadow 沿 canvas 内容 alpha 通道生效,跟随曲线形状
// 对应 CSS: box-shadow: -1px -5px 4px 0px #CF232338 (#CF2323 + alpha 0x38 ≈ 0.22)
filter: drop-shadow(-1px -5px 4px rgba(207, 35, 35, 0.22));
}
.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>