256 lines
6.2 KiB
Vue
256 lines
6.2 KiB
Vue
<template>
|
||
<view class="sixdim-section">
|
||
<view class="sixdim-card">
|
||
<view class="sixdim-info">
|
||
<view class="sixdim-info-icon">i</view>
|
||
</view>
|
||
|
||
<!-- 电脑端雷达图:ECharts 渲染 -->
|
||
<view class="sixdim-radar">
|
||
<view ref="chartRef" class="sixdim-echart-inner" />
|
||
</view>
|
||
|
||
<view class="sixdim-remark sixdim-remark-center">
|
||
<text class="sixdim-remark-label">大师批注:</text>
|
||
<text class="sixdim-remark-text">{{ remark }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { onMounted, onUnmounted, ref, watch } from "vue";
|
||
import * as echarts from "echarts";
|
||
|
||
interface Props {
|
||
labels: string[];
|
||
values: number[];
|
||
remark: string;
|
||
}
|
||
|
||
const props = defineProps<Props>();
|
||
|
||
const chartRef = ref<HTMLElement | null>(null);
|
||
let chart: echarts.ECharts | null = null;
|
||
|
||
const getSafeValues = (vals: number[]) => (Array.isArray(vals) ? vals.map((v) => (Number.isFinite(v as any) ? Number(v) : 0)) : []);
|
||
const getSafeLabels = (labs: string[]) => (Array.isArray(labs) ? labs.map((s) => String(s ?? "")) : []);
|
||
|
||
/** 雷达图 indicator 与 value 必须等长且非空,否则 ECharts radarLayout 会报 push undefined */
|
||
function alignRadarData(rawLabels: string[], rawValues: number[]): { labels: string[]; values: number[] } {
|
||
let labels = getSafeLabels(rawLabels).map((s) => s.trim());
|
||
let values = getSafeValues(rawValues);
|
||
let n = Math.max(labels.length, values.length);
|
||
|
||
// 个人测名默认六维:事业、财运、健康、家庭、社交、智慧
|
||
const defaultSix = ["事业", "财运", "健康", "家庭", "社交", "智慧"];
|
||
|
||
if (n === 0) {
|
||
labels = defaultSix;
|
||
values = [0, 0, 0, 0, 0, 0];
|
||
n = 6;
|
||
} else if (n <= 6) {
|
||
// 不足 6 维时,补足到 6 维并强制使用默认中文维度,确保「家庭」等字段一定出现
|
||
n = 6;
|
||
while (values.length < n) values.push(0);
|
||
labels = defaultSix;
|
||
} else {
|
||
while (labels.length < n) labels.push(`维度${labels.length + 1}`);
|
||
while (values.length < n) values.push(0);
|
||
labels = labels.slice(0, n);
|
||
values = values.slice(0, n);
|
||
}
|
||
|
||
// 再次截断到 n,防御性处理
|
||
labels = labels.slice(0, n).map((s) => s || "—");
|
||
values = values.slice(0, n);
|
||
return { labels, values };
|
||
}
|
||
|
||
const buildOption = () => {
|
||
const { labels, values } = alignRadarData(props.labels, props.values);
|
||
|
||
const radarIndicator = labels.map((name) => ({ name: name || "—", max: 100 }));
|
||
|
||
return {
|
||
animationDuration: 700,
|
||
animationEasing: "cubicOut" as any,
|
||
tooltip: {
|
||
trigger: "item",
|
||
formatter: (params: any) => {
|
||
const valueArr: number[] = params?.value ?? [];
|
||
const parts = labels.map((lab, idx) => `${lab}:${valueArr[idx] ?? 0}`);
|
||
return parts.join("<br/>");
|
||
},
|
||
},
|
||
radar: {
|
||
indicator: radarIndicator,
|
||
splitNumber: 4,
|
||
shape: "polygon",
|
||
center: ["50%", "48%"],
|
||
// 轴文字可能在较小容器内被裁切:适当缩小半径,让 label 落在视窗内
|
||
radius: "62%",
|
||
axisName: {
|
||
color: "rgba(207, 210, 220, 0.88)",
|
||
fontSize: 9,
|
||
padding: 6,
|
||
},
|
||
axisLine: {
|
||
lineStyle: {
|
||
color: "rgba(212, 220, 236, 0.32)",
|
||
width: 1,
|
||
},
|
||
},
|
||
splitLine: {
|
||
lineStyle: {
|
||
color: "rgba(255, 255, 255, 0.12)",
|
||
width: 1,
|
||
},
|
||
},
|
||
splitArea: {
|
||
areaStyle: {
|
||
color: ["rgba(255, 255, 255, 0.02)", "rgba(255, 193, 7, 0.015)"],
|
||
},
|
||
},
|
||
},
|
||
series: [
|
||
{
|
||
type: "radar",
|
||
data: [
|
||
{
|
||
value: [...values],
|
||
},
|
||
],
|
||
lineStyle: {
|
||
color: "rgba(255, 193, 7, 0.92)",
|
||
width: 2,
|
||
},
|
||
itemStyle: {
|
||
color: "rgba(255, 193, 7, 0.95)",
|
||
},
|
||
symbol: "circle",
|
||
symbolSize: 3,
|
||
areaStyle: {
|
||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||
{ offset: 0, color: "rgba(255, 193, 7, 0.78)" },
|
||
{ offset: 1, color: "rgba(243, 138, 43, 0.52)" },
|
||
]),
|
||
opacity: 0.95,
|
||
},
|
||
},
|
||
],
|
||
// 包含所有轴文字(避免 label 在容器内被裁切)
|
||
grid: { containLabel: true },
|
||
};
|
||
};
|
||
|
||
const render = () => {
|
||
if (!chartRef.value) return;
|
||
if (!chart) {
|
||
// 使用 svg 渲染器,避免 canvas getContext 在某些环境出错
|
||
chart = echarts.init(chartRef.value, undefined, { renderer: "svg" });
|
||
}
|
||
chart.setOption(buildOption(), { notMerge: true, lazyUpdate: true });
|
||
};
|
||
|
||
onMounted(() => {
|
||
render();
|
||
|
||
// 适配父容器缩放(桌面布局下尤其必要)
|
||
const onResize = () => chart?.resize();
|
||
window.addEventListener("resize", onResize);
|
||
|
||
onUnmounted(() => window.removeEventListener("resize", onResize));
|
||
});
|
||
|
||
watch(
|
||
() => [props.labels, props.values],
|
||
() => {
|
||
render();
|
||
},
|
||
{ deep: true }
|
||
);
|
||
|
||
onUnmounted(() => {
|
||
if (chart) {
|
||
chart.dispose();
|
||
chart = null;
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<style scoped>
|
||
.sixdim-section {
|
||
margin-bottom: 64rpx;
|
||
}
|
||
|
||
.sixdim-card {
|
||
position: relative;
|
||
background: rgba(255, 255, 255, 0.05);
|
||
border-radius: 20rpx;
|
||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||
padding: 28rpx 24rpx 32rpx;
|
||
backdrop-filter: blur(12px);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.sixdim-info {
|
||
position: absolute;
|
||
top: 16px;
|
||
right: 16rpx;
|
||
}
|
||
|
||
.sixdim-info-icon {
|
||
width: 28px;
|
||
height: 28px;
|
||
border-radius: 999rpx;
|
||
border: 1px solid rgba(255, 255, 255, 0.16);
|
||
color: rgba(255, 255, 255, 0.45);
|
||
font-size: 18px;
|
||
text-align: center;
|
||
}
|
||
|
||
.sixdim-radar {
|
||
position: relative;
|
||
width: 100%;
|
||
max-width: 380rpx;
|
||
margin: 0 auto;
|
||
padding-top: 0;
|
||
background: radial-gradient(circle at 50% 50%, rgba(255, 193, 7, 0.04), transparent 70%);
|
||
border-radius: 24rpx;
|
||
height: 280px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.sixdim-echart-inner {
|
||
position: absolute;
|
||
inset: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
top: 0;
|
||
}
|
||
|
||
.sixdim-remark {
|
||
font-size: 20rpx;
|
||
line-height: 1.8;
|
||
color: #a0a4b8;
|
||
}
|
||
|
||
.sixdim-remark-center {
|
||
margin-top: 24rpx;
|
||
text-align: center;
|
||
padding: 0 24rpx;
|
||
}
|
||
|
||
.sixdim-remark-label {
|
||
color: #fdd835;
|
||
}
|
||
|
||
.sixdim-remark-text {
|
||
color: #c0c3d0;
|
||
}
|
||
</style>
|
||
|