257 lines
6.6 KiB
Vue
257 lines
6.6 KiB
Vue
<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 已 patch:series.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>
|