1135 lines
34 KiB
Vue
1135 lines
34 KiB
Vue
<template>
|
||
<div class="step4" ref="rootEl">
|
||
<div class="top nav">
|
||
<div class="l"></div>
|
||
<div class="title">
|
||
我的检测报告
|
||
</div>
|
||
<div class="r" @click="router.replace('/')"><img src="@/assets/close.png" alt=""></div>
|
||
</div>
|
||
<div class="time"> 检测时间:<span>{{ creatTime }}</span></div>
|
||
|
||
<div class="tabs" ref="tabsEl">
|
||
<div :class="['tab', activeTab === value.id ? 'active' : '']" v-for="value in tabs" :key="value.id"
|
||
@click="handleTabClick(value.id)">{{ value.name }}</div>
|
||
</div>
|
||
<div class="content content1" :ref="(el) => setSectionEl(1, el)">
|
||
<Item title="综合报告">
|
||
<div class="text text1"> 您的健康状况一般,{{ numsData.abnormalTotal }}项指标超出正常范围,需要引起重视。</div>
|
||
<div class="indicator">
|
||
<div class="chart" ref="chartWrapEl">
|
||
<canvas ref="radarCanvasRef"></canvas>
|
||
</div>
|
||
<div class="r">
|
||
<div class="r-item">
|
||
<div>
|
||
<span>{{ numsData.normalTotal }}</span>项<br> 正常指标
|
||
</div>
|
||
|
||
</div>
|
||
<div class="r-item">
|
||
<div>
|
||
<span class="red">{{ numsData.abnormalTotal }}</span>项<br> 异常指标
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="text">
|
||
生命体征指标正常,心肺功能良好。血液指标方面,血糖超出正常范围,建议调整饮食结构,定期复查。心理健康方面,压力指数、焦虑指数偏高,建议适当放松,保持积极心态皮肤健康方面,您的皮肤整体状态良好,含水量适中。面部有轻度黑眼圈,可能与睡眠不足有关。T区出油较为明显,建议加强控油和清洁。面部有少量痤疮,需注意饮食清淡和规律作息。整体肤质为混合型,建议分区护理。
|
||
</div>
|
||
</Item>
|
||
</div>
|
||
|
||
<div class="content content2" :ref="(el) => setSectionEl(2, el)">
|
||
<Item title="基础体征分析报告">
|
||
<div class="card first">
|
||
<div class="top">
|
||
<div class="icon"><img src="@/assets/images/icon1.png" alt=""></div>
|
||
<div class="title">心率</div>
|
||
<div :class="['status', data?.metrics?.vital_signs?.heart_rate?.status == '正常' ? '' : 'error']">{{ data?.metrics?.vital_signs?.heart_rate?.status }}</div>
|
||
</div>
|
||
<div class="middle">心率 <span>{{ data?.metrics?.vital_signs?.heart_rate?.value }}</span> bpm</div>
|
||
<div class="bottom">正常范围:60~100 bmp</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="top">
|
||
<div class="icon"><img src="@/assets/images/icon2.png" alt=""></div>
|
||
<div class="title">呼吸频率</div>
|
||
<div :class="['status', data?.metrics?.vital_signs?.respiratory_rate?.status == '正常' ? '' : 'error']">{{ data?.metrics?.vital_signs?.respiratory_rate?.status}}</div>
|
||
</div>
|
||
<div class="middle">呼吸频率 <span>{{ data?.metrics?.vital_signs?.respiratory_rate?.value }}</span> rpm</div>
|
||
<div class="bottom">正常范围:12~20 rpm</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="top">
|
||
<div class="icon"><img src="@/assets/images/icon3.png" alt=""></div>
|
||
<div class="title">舒张压</div>
|
||
<div :class="['status', data?.metrics?.vital_signs?.diastolic_bp?.status == '正常' ? '' : 'error']">{{ data?.metrics?.vital_signs?.diastolic_bp?.status }}</div>
|
||
</div>
|
||
<div class="middle">舒张压 <span>{{ data?.metrics?.vital_signs?.diastolic_bp?.value }}</span> rpm</div>
|
||
<div class="bottom">正常范围:12~20 rpm</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="top">
|
||
<div class="icon"><img src="@/assets/images/icon4.png" alt=""></div>
|
||
<div class="title">收缩压</div>
|
||
<div :class="['status', data?.metrics?.vital_signs?.systolic_bp?.status == '正常' ? '' : 'error']">{{ data?.metrics?.vital_signs?.systolic_bp?.status }}</div>
|
||
</div>
|
||
<div class="middle">舒张压 <span>{{ data?.metrics?.vital_signs?.systolic_bp?.value }}</span> mmHg</div>
|
||
<div class="bottom">正常范围:12~20 rpm</div>
|
||
</div>
|
||
<div class="tips">
|
||
<Tips title="健康提示" :text="data?.metrics?.vital_signs?.analysis" />
|
||
</div>
|
||
</Item>
|
||
</div>
|
||
<div class="content content3" :ref="(el) => setSectionEl(3, el)">
|
||
<Item title="血液健康分析报告">
|
||
<div class="card">
|
||
<div class="top">
|
||
<div class="icon"><img src="@/assets/images/icon4.png" alt=""></div>
|
||
<div class="title">血糖</div>
|
||
<div :class="['status', data?.metrics?.blood_health?.glucose?.status == '偏低' ? 'status1' : data?.metrics?.blood_health?.glucose?.status == '偏高' ? 'status3' : 'status2']">{{ data?.metrics?.blood_health?.glucose?.status }}</div>
|
||
</div>
|
||
<div class="middle"> <span>{{ data.metrics?.blood_health?.glucose?.value }}</span> bmp</div>
|
||
<div :class="['middle_progress', data?.metrics?.blood_health?.glucose?.status == '偏低' ? 'status1' : data?.metrics?.blood_health?.glucose?.status == '偏高' ? 'status3' : 'status2']"></div>
|
||
<div class="bottom">正常范围:3.9~6.1 mmol/L</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="top">
|
||
<div class="icon"><img src="@/assets/images/icon4.png" alt=""></div>
|
||
<div class="title">血红蛋白</div>
|
||
<div :class="['status', data?.metrics?.blood_health?.hemoglobin?.status == '偏低' ? 'status1' : data?.metrics?.blood_health?.hemoglobin?.status == '偏高' ? 'status3' : 'status2']">{{ data?.metrics?.blood_health?.hemoglobin?.status }}</div>
|
||
</div>
|
||
<div class="middle"> <span>{{ data.metrics?.blood_health?.hemoglobin?.value }}</span> rmp</div>
|
||
<div :class="['middle_progress', data?.metrics?.blood_health?.hemoglobin?.status == '偏低' ? 'status1' : data?.metrics?.blood_health?.hemoglobin?.status == '偏高' ? 'status3' : 'status2']"></div>
|
||
<div class="bottom">正常范围:110~165 g/L</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="top">
|
||
<div class="icon"><img src="@/assets/images/icon4.png" alt=""></div>
|
||
<div class="title">甘油三酯</div>
|
||
<div :class="['status', data?.metrics?.blood_health?.triglycerides?.status == '偏低' ? 'status1' : data?.metrics?.blood_health?.triglycerides?.status == '偏高' ? 'status3' : 'status2']">{{ data?.metrics?.blood_health?.triglycerides?.status }}</div>
|
||
</div>
|
||
<div class="middle"> <span>{{ data?.metrics?.blood_health?.triglycerides?.value }}</span> mmol/L</div>
|
||
<div :class="['middle_progress', data?.metrics?.blood_health?.triglycerides?.status == '偏低' ? 'status1' : data?.metrics?.blood_health?.triglycerides?.status == '偏高' ? 'status3' : 'status2']"></div>
|
||
<div class="bottom">正常范围:0.565 ~ 1.96 mmol/L</div>
|
||
</div>
|
||
<div class="tips">
|
||
<Tips title="健康提示" :text="data?.metrics?.blood_health?.analysis" />
|
||
</div>
|
||
</Item>
|
||
</div>
|
||
|
||
<div class="content content4" :ref="(el) => setSectionEl(4, el)">
|
||
<Item title="心理健康分析报告">
|
||
<div class="card">
|
||
<div class="top">
|
||
心理健康指数
|
||
</div>
|
||
<div class="middle"> <span>{{ data?.metrics?.mental_health?.mental_score?.value }}</span> 分</div>
|
||
<div class="middle_progress">
|
||
<Progress color="linear-gradient(to right, #F5EBC2, #C8A92E)" :percentage="data?.metrics?.mental_health?.mental_score?.value" :pivot-text="''" />
|
||
</div>
|
||
<div class="bottom">正常范围:70-100分</div>
|
||
</div>
|
||
<div class="indicator">
|
||
<div class="indicator_item">
|
||
<div>
|
||
<div class="title"> <img src="@/assets/images/content4-icon1.png" alt="">压力指数
|
||
</div>
|
||
<div class="value">
|
||
<span :class="['value', data?.metrics?.mental_health?.stress?.status == '正常' ? '' : 'error']">{{ data?.metrics?.mental_health?.stress?.value }}</span>
|
||
<div :class="['status', data?.metrics?.mental_health?.stress?.status == '正常' ? '' : 'error']">{{ data?.metrics?.mental_health?.stress?.status=='正常' ? '正常' : '异常' }}</div>
|
||
</div>
|
||
<div class="desc">正常范围:0~5分</div>
|
||
</div>
|
||
</div>
|
||
<div class="indicator_item">
|
||
<div>
|
||
<div class="title"> <img src="@/assets/images/content4-icon1.png" alt="">抑郁指数
|
||
</div>
|
||
<div class="value">
|
||
<span :class="['value', data?.metrics?.mental_health?.depression?.status == '正常' ? '' : 'error']">{{ data?.metrics?.mental_health?.depression?.value }}</span>
|
||
<div :class="['status', data?.metrics?.mental_health?.depression?.status == '正常' ? '' : 'error']">{{ data?.metrics?.mental_health?.depression?.status=='正常' ? '正常' : '异常' }}</div>
|
||
</div>
|
||
<div class="desc">正常范围:0~3分</div>
|
||
</div>
|
||
</div>
|
||
<div class="indicator_item">
|
||
<div>
|
||
<div class="title"> <img src="@/assets/images/content4-icon1.png" alt="">焦虑指数
|
||
</div>
|
||
<div class="value">
|
||
<span :class="['value', data?.metrics?.mental_health?.anxiety?.status == '正常' ? '' : 'error']">{{ data?.metrics?.mental_health?.anxiety?.value }}</span>
|
||
<div :class="['status', data?.metrics?.mental_health?.anxiety?.status == '正常' ? '' : 'error']">{{ data?.metrics?.mental_health?.anxiety?.status=='正常' ? '正常' : '异常' }}</div>
|
||
</div>
|
||
<div class="desc">正常范围:0~3分</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- <div class="tips1"></div> -->
|
||
<div class="tips2">
|
||
<Tips title="健康提示" :text="data?.metrics?.mental_health?.analysis" />
|
||
</div>
|
||
</Item>
|
||
</div>
|
||
<div class="content content5" :ref="(el) => setSectionEl(5, el)">
|
||
<Item title="皮肤健康分析报告">
|
||
<div class="card">
|
||
<div class="type"><span>混合型肤质</span></div>
|
||
<div class="desc">混合性肌肤:T区偏油,U区偏干</div>
|
||
|
||
<div class="line-box">
|
||
<div class="dot" :style="{'--left': data?.metrics?.skin_status?.hydration?.value + '%'}">皮肤含水量{{data?.metrics?.skin_status?.hydration?.value}}%</div>
|
||
<div class="line"></div>
|
||
</div>
|
||
|
||
<div class="skin-box">
|
||
<div class="avatar">
|
||
<div class="item">黑眼圈 <img :src="data?.metrics?.skin_status?.dark_circles?.value =='轻度' ? content5Status1 : data?.metrics?.skin_status?.dark_circles?.value =='中度' ? content5Status2 : content5Status3" alt=""></div>
|
||
<div class="item item2">出油状态 <img :src="data?.metrics?.skin_status?.oil_control?.value =='轻度' ? content5Status1 : data?.metrics?.skin_status?.oil_control?.value =='中度' ? content5Status2 : content5Status3" alt=""></div>
|
||
<div class="item item3">痤疮 <img :src="data?.metrics?.skin_status?.acne?.value =='少量' ? content5Status1 : data?.metrics?.skin_status?.acne?.value =='中度' ? content5Status2 : content5Status3" alt=""></div>
|
||
</div>
|
||
<div class="legend"></div>
|
||
</div>
|
||
<div class="text">您的皮肤整体状态良好,含水量适中。面部有轻度黑眼圈,可能与睡眠不足有关。T区出油较为明显,建议加强控油和清洁。面部有少量痤疮,需注意饮食清淡和规律作息。整体肤质为混合型,建议分区护理。</div>
|
||
</div>
|
||
<div class="tips">
|
||
<Tips title="护理建议" :text="data?.metrics?.skin_status?.analysis" />
|
||
</div>
|
||
</Item>
|
||
</div>
|
||
|
||
</div>
|
||
</template>
|
||
<script setup lang="ts">
|
||
import { Progress } from 'vant';
|
||
import Tips from '@/components/Tips/index.vue';
|
||
import { useRouter } from 'vue-router';
|
||
import { onBeforeUnmount, onMounted, ref, watch, nextTick } from 'vue';
|
||
import { format } from 'silly-datetime';
|
||
import Item from '@/components/Item/index.vue';
|
||
import content5Status1 from '@/assets/images/content5-status1.png';
|
||
import content5Status2 from '@/assets/images/content5-status2.png';
|
||
import content5Status3 from '@/assets/images/content5-status3.png';
|
||
const router = useRouter();
|
||
const creatTime = ref(new Date().toLocaleString());
|
||
const data = ref<any>({});
|
||
const summaryText = ref('');
|
||
const numsData = ref<any>({
|
||
base:{
|
||
normal:0,//正常
|
||
abnormal:0,//异常
|
||
},
|
||
blood:{
|
||
normal:0,//正常
|
||
abnormal:0,//异常
|
||
},
|
||
mental:{
|
||
normal:0,//正常
|
||
abnormal:0,//异常
|
||
},
|
||
normalTotal:0,//正常总数
|
||
abnormalTotal:0,//异常总数
|
||
});
|
||
|
||
const tabs = ref([
|
||
{
|
||
name: '综合报告',
|
||
id: 1,
|
||
},
|
||
{
|
||
name: '基础体征',
|
||
id: 2,
|
||
},
|
||
{
|
||
name: '血液健康',
|
||
id: 3,
|
||
},
|
||
{
|
||
name: '心理健康',
|
||
id: 4,
|
||
}, {
|
||
name: '皮肤状态',
|
||
id: 5,
|
||
}
|
||
]);
|
||
const activeTab = ref(1);
|
||
|
||
const rootEl = ref<HTMLElement | null>(null);
|
||
const tabsEl = ref<HTMLElement | null>(null);
|
||
const chartWrapEl = ref<HTMLElement | null>(null);
|
||
const radarCanvasRef = ref<HTMLCanvasElement | null>(null);
|
||
|
||
/** 各维度正常项占比 0–100,用于雷达轴长 */
|
||
const categoryRadarScore = (cat: { normal: number; abnormal: number }) => {
|
||
const total = cat.normal + cat.abnormal;
|
||
if (total <= 0) return 100;
|
||
return (cat.normal / total) * 100;
|
||
};
|
||
|
||
const drawRadarChart = () => {
|
||
const wrap = chartWrapEl.value;
|
||
const canvas = radarCanvasRef.value;
|
||
if (!wrap || !canvas) return;
|
||
|
||
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||
const w = wrap.clientWidth;
|
||
const h = wrap.clientHeight;
|
||
if (w < 2 || h < 2) return;
|
||
|
||
canvas.width = Math.round(w * dpr);
|
||
canvas.height = Math.round(h * dpr);
|
||
canvas.style.width = `${w}px`;
|
||
canvas.style.height = `${h}px`;
|
||
|
||
const ctx = canvas.getContext('2d');
|
||
if (!ctx) return;
|
||
|
||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||
|
||
ctx.clearRect(0, 0, w, h);
|
||
|
||
const cx = w / 2;
|
||
const cy = h / 2;
|
||
// 留出标签边距
|
||
const labelPad = Math.min(w, h) * 0.12;
|
||
const R = Math.min(w, h) / 2 - labelPad;
|
||
|
||
const angles = [
|
||
-Math.PI / 2, // 基础体征 12 点
|
||
Math.PI / 6, // 血液 右下
|
||
(5 * Math.PI) / 6, // 心理 左下
|
||
];
|
||
|
||
const maxVal = 100;
|
||
const base = categoryRadarScore(numsData.value.base);
|
||
const blood = categoryRadarScore(numsData.value.blood);
|
||
const mental = categoryRadarScore(numsData.value.mental);
|
||
const values = [base, blood, mental];
|
||
|
||
const ax = (i: number) => cx + R * Math.cos(angles[i]!);
|
||
const ay = (i: number) => cy + R * Math.sin(angles[i]!);
|
||
|
||
const px = (i: number) => {
|
||
const t = Math.min(Math.max(values[i]! / maxVal, 0), 1);
|
||
return cx + R * t * Math.cos(angles[i]!);
|
||
};
|
||
const py = (i: number) => {
|
||
const t = Math.min(Math.max(values[i]! / maxVal, 0), 1);
|
||
return cy + R * t * Math.sin(angles[i]!);
|
||
};
|
||
|
||
const gridColor = 'rgba(0, 0, 0, 0.08)';
|
||
const goldStroke = '#C8A92E';
|
||
const goldFill = 'rgba(200, 169, 46, 0.22)';
|
||
const goldDeep = 'rgba(160, 120, 30, 0.35)';
|
||
|
||
// 网格:3 层同心圆 + 轴线
|
||
ctx.strokeStyle = gridColor;
|
||
ctx.lineWidth = 1;
|
||
for (let level = 1; level <= 3; level++) {
|
||
const r = (R * level) / 3;
|
||
ctx.beginPath();
|
||
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
||
ctx.stroke();
|
||
}
|
||
for (let i = 0; i < 3; i++) {
|
||
ctx.beginPath();
|
||
ctx.moveTo(cx, cy);
|
||
ctx.lineTo(ax(i), ay(i));
|
||
ctx.stroke();
|
||
}
|
||
|
||
const p: [number, number][] = [
|
||
[px(0), py(0)],
|
||
[px(1), py(1)],
|
||
[px(2), py(2)],
|
||
];
|
||
|
||
// 分三块扇形填充,心理扇区略深以增加层次
|
||
const fillWedge = (i0: number, i1: number, alphaMul: number, useDeep: boolean) => {
|
||
ctx.beginPath();
|
||
ctx.moveTo(cx, cy);
|
||
ctx.lineTo(p[i0]![0], p[i0]![1]);
|
||
ctx.lineTo(p[i1]![0], p[i1]![1]);
|
||
ctx.closePath();
|
||
ctx.fillStyle = useDeep ? goldDeep : goldFill;
|
||
ctx.globalAlpha = alphaMul;
|
||
ctx.fill();
|
||
ctx.globalAlpha = 1;
|
||
};
|
||
|
||
fillWedge(0, 1, 1, false);
|
||
fillWedge(1, 2, 1, false);
|
||
fillWedge(2, 0, 1, true);
|
||
|
||
ctx.beginPath();
|
||
ctx.moveTo(p[0]![0], p[0]![1]);
|
||
ctx.lineTo(p[1]![0], p[1]![1]);
|
||
ctx.lineTo(p[2]![0], p[2]![1]);
|
||
ctx.closePath();
|
||
ctx.strokeStyle = goldStroke;
|
||
ctx.lineWidth = 1.5;
|
||
ctx.stroke();
|
||
|
||
ctx.fillStyle = '#555';
|
||
ctx.font = `${Math.round(Math.min(w, h) * 0.045)}px sans-serif`;
|
||
ctx.textAlign = 'center';
|
||
ctx.textBaseline = 'middle';
|
||
|
||
const labels = ['基础体征', '血液指标', '心理健康'];
|
||
const labelR = R + labelPad * 0.55;
|
||
for (let i = 0; i < 3; i++) {
|
||
const lx = cx + labelR * Math.cos(angles[i]!);
|
||
const ly = cy + labelR * Math.sin(angles[i]!);
|
||
ctx.fillText(labels[i]!, lx, ly);
|
||
}
|
||
};
|
||
|
||
let radarRo: ResizeObserver | null = null;
|
||
|
||
const scheduleDrawRadar = () => {
|
||
nextTick(() => drawRadarChart());
|
||
};
|
||
const sectionEls = new Map<number, HTMLElement>();
|
||
const isProgrammaticScrolling = ref(false);
|
||
const scrollContainer = ref<Window | HTMLElement>(window);
|
||
let rafId: number | null = null;
|
||
let programmaticTimer: number | null = null;
|
||
|
||
const setSectionEl = (id: number, el: unknown) => {
|
||
if (!(el instanceof HTMLElement)) return;
|
||
sectionEls.set(id, el);
|
||
};
|
||
|
||
const getScrollParent = (el: HTMLElement | null) => {
|
||
let cur: HTMLElement | null = el?.parentElement ?? null;
|
||
while (cur) {
|
||
const style = window.getComputedStyle(cur);
|
||
const overflowY = style.overflowY;
|
||
const canScroll =
|
||
(overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'overlay') &&
|
||
cur.scrollHeight > cur.clientHeight + 1;
|
||
if (canScroll) return cur;
|
||
cur = cur.parentElement;
|
||
}
|
||
return window;
|
||
};
|
||
|
||
const getStickyOffset = () => {
|
||
// tabs 吸顶后会遮住内容,这里用 tabs 高度做滚动偏移
|
||
const tabsHeight = tabsEl.value?.getBoundingClientRect().height ?? 0;
|
||
return Math.ceil(tabsHeight + 12); // 额外留一点间距,避免标题被顶得太紧
|
||
};
|
||
|
||
const scrollToSection = (id: number) => {
|
||
const el = sectionEls.get(id);
|
||
if (!el) return;
|
||
|
||
const offset = getStickyOffset();
|
||
const container = scrollContainer.value;
|
||
|
||
if (container === window) {
|
||
const top = window.scrollY + el.getBoundingClientRect().top - offset;
|
||
window.scrollTo({ top: Math.max(0, top), behavior: 'smooth' });
|
||
return;
|
||
}
|
||
|
||
if (container instanceof HTMLElement) {
|
||
const containerRect = container.getBoundingClientRect();
|
||
const top = container.scrollTop + (el.getBoundingClientRect().top - containerRect.top) - offset;
|
||
container.scrollTo({ top: Math.max(0, top), behavior: 'smooth' });
|
||
}
|
||
};
|
||
|
||
const handleTabClick = (id: number) => {
|
||
activeTab.value = id;
|
||
isProgrammaticScrolling.value = true;
|
||
if (programmaticTimer) window.clearTimeout(programmaticTimer);
|
||
programmaticTimer = window.setTimeout(() => {
|
||
isProgrammaticScrolling.value = false;
|
||
}, 800);
|
||
scrollToSection(id);
|
||
};
|
||
|
||
const updateActiveTabByScroll = () => {
|
||
if (isProgrammaticScrolling.value) return;
|
||
|
||
const offset = getStickyOffset();
|
||
const ids = tabs.value.map((t) => t.id);
|
||
|
||
let current = ids[0] ?? 1;
|
||
for (const id of ids) {
|
||
const el = sectionEls.get(id);
|
||
if (!el) continue;
|
||
const top = el.getBoundingClientRect().top;
|
||
if (top - offset <= 2) current = id;
|
||
else break;
|
||
}
|
||
|
||
activeTab.value = current;
|
||
};
|
||
|
||
const onScroll = () => {
|
||
if (rafId != null) return;
|
||
rafId = window.requestAnimationFrame(() => {
|
||
rafId = null;
|
||
updateActiveTabByScroll();
|
||
});
|
||
};
|
||
|
||
onMounted(() => {
|
||
scrollContainer.value = getScrollParent(rootEl.value);
|
||
const container = scrollContainer.value;
|
||
if (container === window) window.addEventListener('scroll', onScroll, { passive: true });
|
||
else container.addEventListener('scroll', onScroll, { passive: true });
|
||
// 首次进入时根据当前位置更新一次
|
||
updateActiveTabByScroll();
|
||
|
||
scheduleDrawRadar();
|
||
if (typeof ResizeObserver !== 'undefined' && chartWrapEl.value) {
|
||
radarRo = new ResizeObserver(() => drawRadarChart());
|
||
radarRo.observe(chartWrapEl.value);
|
||
}
|
||
window.addEventListener('resize', drawRadarChart);
|
||
});
|
||
|
||
watch(
|
||
numsData,
|
||
() => scheduleDrawRadar(),
|
||
{ deep: true },
|
||
);
|
||
|
||
onBeforeUnmount(() => {
|
||
const container = scrollContainer.value;
|
||
if (container === window) window.removeEventListener('scroll', onScroll);
|
||
else container.removeEventListener('scroll', onScroll);
|
||
if (rafId != null) window.cancelAnimationFrame(rafId);
|
||
if (programmaticTimer) window.clearTimeout(programmaticTimer);
|
||
window.removeEventListener('resize', drawRadarChart);
|
||
radarRo?.disconnect();
|
||
radarRo = null;
|
||
});
|
||
//真实数据
|
||
const arkResult = sessionStorage.getItem('step2_ark_result');
|
||
if (arkResult) {
|
||
const result = JSON.parse(arkResult);
|
||
|
||
console.log(result);
|
||
|
||
summaryText.value = result?.output[0]?.summary[0]?.text;
|
||
console.log(summaryText.value); //文本
|
||
|
||
data.value = JSON.parse(result?.output[1]?.content[0]?.text);
|
||
console.log(data.value);
|
||
|
||
/** 只统计带 status 的指标项,跳过 analysis 等非指标字段 */
|
||
const countStatus = (section: Record<string, unknown> | undefined, bucket: { normal: number; abnormal: number }) => {
|
||
if (!section || typeof section !== 'object') return;
|
||
for (const key of Object.keys(section)) {
|
||
const item = section[key];
|
||
if (!item || typeof item !== 'object' || !('status' in item)) continue;
|
||
if ((item as { status: string }).status === '正常') bucket.normal++;
|
||
else bucket.abnormal++;
|
||
}
|
||
};
|
||
|
||
countStatus(data.value?.metrics?.vital_signs as Record<string, unknown> | undefined, numsData.value.base);
|
||
countStatus(data.value?.metrics?.blood_health as Record<string, unknown> | undefined, numsData.value.blood);
|
||
countStatus(data.value?.metrics?.mental_health as Record<string, unknown> | undefined, numsData.value.mental);
|
||
|
||
numsData.value.normalTotal = numsData.value.base.normal + numsData.value.blood.normal + numsData.value.mental.normal;
|
||
numsData.value.abnormalTotal = numsData.value.base.abnormal + numsData.value.blood.abnormal + numsData.value.mental.abnormal;
|
||
|
||
console.log(numsData.value);
|
||
|
||
creatTime.value = format(new Date(result.created_at ? result.created_at * 1000 : new Date()), 'YYYY-MM-DD HH:mm:ss')
|
||
summaryText.value = data.value?.brief_report?.summary_text;
|
||
|
||
}
|
||
</script>
|
||
<style scoped lang="scss">
|
||
.step4 {
|
||
background: #F3F3F3;
|
||
min-height: 100vh;
|
||
padding-top: 52px;
|
||
box-sizing: border-box;
|
||
|
||
.top.nav {
|
||
padding: 0 30px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
font-size: 50px;
|
||
|
||
.l {
|
||
width: 60px;
|
||
height: 60px;
|
||
}
|
||
|
||
.r {
|
||
width: 60px;
|
||
height: 60px;
|
||
|
||
img {
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
.title {
|
||
background: url('@/assets/images/indicator-bg.png') no-repeat center center / cover;
|
||
|
||
}
|
||
}
|
||
|
||
.time {
|
||
line-height: 33px;
|
||
margin-top: 3px;
|
||
text-align: center;
|
||
font-size: 24px;
|
||
color: #000;
|
||
|
||
span {
|
||
color: #797979;
|
||
}
|
||
}
|
||
|
||
.tabs {
|
||
margin: 50px auto 0;
|
||
width: 956px;
|
||
height: 118px;
|
||
border-radius: 60px;
|
||
border: 2px solid #000;
|
||
display: flex;
|
||
justify-content: space-around;
|
||
align-items: center;
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 20;
|
||
background: #F3F3F3;
|
||
|
||
.tab {
|
||
font-size: 30px;
|
||
text-align: center;
|
||
position: relative;
|
||
|
||
&:before {
|
||
content: '';
|
||
position: absolute;
|
||
height: 30px;
|
||
width: 4px;
|
||
background-color: #D8D8D8;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
left: -34px;
|
||
}
|
||
|
||
&:nth-child(1) {
|
||
&:before {
|
||
display: none;
|
||
}
|
||
}
|
||
|
||
&.active {
|
||
font-weight: 900;
|
||
|
||
&:after {
|
||
content: '';
|
||
position: absolute;
|
||
height: 4px;
|
||
width: 46px;
|
||
background-color: #000;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
bottom: -12px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.content1 {
|
||
margin-top: 40px;
|
||
|
||
.text {
|
||
font-size: 30px;
|
||
}
|
||
|
||
.text1 {
|
||
margin-top: 30px;
|
||
}
|
||
|
||
.indicator {
|
||
margin: 55px 0;
|
||
display: flex;
|
||
|
||
.chart {
|
||
margin-right: 80px;
|
||
flex: 1;
|
||
height: 325px;
|
||
min-width: 0;
|
||
|
||
canvas {
|
||
display: block;
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
}
|
||
|
||
.r {
|
||
width: 258px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: space-between;
|
||
|
||
.r-item {
|
||
width: 100%;
|
||
height: 140px;
|
||
background: #F8F8F8;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
font-size: 24px;
|
||
color: #000;
|
||
|
||
span {
|
||
font-size: 72px;
|
||
font-weight: 900;
|
||
color: #000;
|
||
|
||
&.red {
|
||
color: #F36151;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.content2,
|
||
.content3,
|
||
.content4,
|
||
.content5 {
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.content2 {
|
||
.card {
|
||
&.first {
|
||
margin-top: 30px;
|
||
}
|
||
|
||
border-radius: 18px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: space-between;
|
||
padding: 30px 50px;
|
||
margin-top: 20px;
|
||
height: 246px;
|
||
background: url('@/assets/images/card-bg.png') no-repeat center center / cover;
|
||
|
||
.top {
|
||
display: flex;
|
||
align-items: center;
|
||
|
||
.icon {
|
||
background: #000;
|
||
border-radius: 50%;
|
||
width: 58px;
|
||
height: 58px;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
|
||
img {
|
||
width: 36px;
|
||
}
|
||
}
|
||
|
||
.title {
|
||
flex: 1;
|
||
margin-left: 12px;
|
||
font-size: 30px;
|
||
}
|
||
|
||
.status {
|
||
width: 89px;
|
||
height: 58px;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
font-size: 24px;
|
||
background: rgba(37, 203, 171, .1);
|
||
color: #25CBAB;
|
||
|
||
&.error {
|
||
background: rgba(243, 93, 81, .1);
|
||
color: #F36151;
|
||
}
|
||
}
|
||
}
|
||
|
||
.middle {
|
||
font-size: 30px;
|
||
color: #000;
|
||
|
||
span {
|
||
font-size: 53px;
|
||
font-weight: 900;
|
||
color: #000;
|
||
}
|
||
}
|
||
|
||
.bottom {
|
||
font-size: 30px;
|
||
color: #000;
|
||
}
|
||
}
|
||
|
||
.tips {
|
||
margin-top: 20px;
|
||
|
||
}
|
||
}
|
||
|
||
.content3 {
|
||
.card {
|
||
&.first {
|
||
margin-top: 30px;
|
||
}
|
||
|
||
border-radius: 18px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: space-between;
|
||
padding: 30px 50px;
|
||
margin-top: 20px;
|
||
height: 317px;
|
||
background: url('@/assets/images/card-bg.png') no-repeat center center / cover;
|
||
|
||
.top {
|
||
display: flex;
|
||
align-items: center;
|
||
|
||
.icon {
|
||
background: #000;
|
||
border-radius: 50%;
|
||
width: 58px;
|
||
height: 58px;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
|
||
img {
|
||
width: 36px;
|
||
}
|
||
}
|
||
|
||
.title {
|
||
flex: 1;
|
||
margin-left: 12px;
|
||
font-size: 30px;
|
||
}
|
||
|
||
.status {
|
||
width: 89px;
|
||
height: 58px;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
font-size: 24px;
|
||
background: rgba(37, 203, 171, .1);
|
||
color: #25CBAB;
|
||
|
||
&.status1 {
|
||
background: rgba(226, 207, 137, .22);
|
||
color: #BBA867;
|
||
}
|
||
|
||
&.status2 {
|
||
background: rgba(37, 203, 171, .1);
|
||
color: #25CBAB;
|
||
}
|
||
|
||
&.status3 {
|
||
background: rgba(243, 93, 81, .1);
|
||
color: #F36151;
|
||
}
|
||
}
|
||
}
|
||
|
||
.middle {
|
||
font-size: 30px;
|
||
color: #000;
|
||
|
||
span {
|
||
font-size: 53px;
|
||
font-weight: 900;
|
||
color: #000;
|
||
}
|
||
}
|
||
|
||
.middle_progress {
|
||
height: 43px;
|
||
background: url('@/assets/images/status1.png') no-repeat center center / cover;
|
||
|
||
&.status2 {
|
||
background: url('@/assets/images/status2.png') no-repeat center center / cover;
|
||
}
|
||
|
||
&.status3 {
|
||
background: url('@/assets/images/status3.png') no-repeat center center / cover;
|
||
}
|
||
}
|
||
|
||
.bottom {
|
||
font-size: 30px;
|
||
color: #000;
|
||
}
|
||
}
|
||
|
||
.tips {
|
||
margin-top: 20px;
|
||
|
||
}
|
||
}
|
||
|
||
.content4 {
|
||
.card {
|
||
&.first {
|
||
margin-top: 30px;
|
||
}
|
||
|
||
border-radius: 18px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: space-between;
|
||
padding: 30px 50px;
|
||
margin-top: 20px;
|
||
height: 317px;
|
||
background: url('@/assets/images/card-bg.png') no-repeat center center / cover;
|
||
|
||
.top {
|
||
font-size: 30px;
|
||
}
|
||
|
||
.middle {
|
||
font-size: 30px;
|
||
color: #000;
|
||
|
||
span {
|
||
font-size: 53px;
|
||
font-weight: 900;
|
||
color: #000;
|
||
}
|
||
}
|
||
|
||
.bottom {
|
||
font-size: 30px;
|
||
color: #000;
|
||
}
|
||
}
|
||
|
||
.indicator {
|
||
margin-top: 20px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
|
||
.indicator_item {
|
||
width: 272px;
|
||
height: 233px;
|
||
background: url('@/assets/images/card-bg.png') no-repeat center center / cover;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
|
||
.title {
|
||
white-space: nowrap;
|
||
font-size: 30px;
|
||
|
||
img {
|
||
display: inline-block;
|
||
width: 30px;
|
||
margin-right: 8px;
|
||
}
|
||
}
|
||
|
||
.value {
|
||
margin: 10px 0;
|
||
display: flex;
|
||
align-items: flex-end;
|
||
|
||
span {
|
||
font-size: 60px;
|
||
&.error {
|
||
color: #F36151;
|
||
}
|
||
}
|
||
|
||
.status {
|
||
margin-left: 8px;
|
||
width: 40px;
|
||
height: 24px;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
font-size: 16px;
|
||
background: rgba(37, 203, 171, .1);
|
||
color: #25CBAB;
|
||
|
||
&.error {
|
||
background: rgba(243, 93, 81, .1);
|
||
color: #F36151;
|
||
}
|
||
}
|
||
}
|
||
|
||
.desc {
|
||
font-size: 24px;
|
||
}
|
||
}
|
||
}
|
||
|
||
// .tips1 {
|
||
// margin-top: 20px;
|
||
// height: 138px;
|
||
// background: url('@/assets/images/content4-tips1.png') no-repeat center center / cover;
|
||
// }
|
||
|
||
.tips2 {
|
||
margin-top: 20px;
|
||
// height: 254px;
|
||
// background: url('@/assets/images/content4-tips2.png') no-repeat center center / cover;
|
||
}
|
||
}
|
||
|
||
.content5 {
|
||
.card {
|
||
margin-top: 30px;
|
||
padding: 50px;
|
||
border-radius: 18px;
|
||
background: linear-gradient(to bottom, #F4F4F4 0%, #fbfbfb 100%);
|
||
|
||
.type {
|
||
font-size: 30px;
|
||
font-weight: 850;
|
||
color: #000;
|
||
position: relative;
|
||
|
||
span {
|
||
z-index: 2;
|
||
display: inline;
|
||
position: relative;
|
||
|
||
&::before {
|
||
z-index: -1;
|
||
border-radius: 10px;
|
||
content: '';
|
||
position: absolute;
|
||
width: 120%;
|
||
height: 20px;
|
||
background: linear-gradient(to right, #F5EBC2 0%, #C8A92E 100%);
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
bottom: -10px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.desc {
|
||
font-size: 24px;
|
||
color: #000;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.line-box {
|
||
margin-top: 96px;
|
||
position: relative;
|
||
|
||
.dot {
|
||
transform: translateX(-50%);
|
||
left: var(--left);
|
||
top: -64px;
|
||
position: absolute;
|
||
width: 208px;
|
||
height: 54px;
|
||
display: inline-block;
|
||
font-size: 24px;
|
||
color:#fff;
|
||
background: url('@/assets/images/dot.png') no-repeat center center / 100% 100%;
|
||
display: flex;
|
||
justify-content: center;
|
||
padding-top: 5px;
|
||
}
|
||
|
||
.line {
|
||
|
||
height: 20px;
|
||
background: url('@/assets/images/content5-line.png') no-repeat center center / cover;
|
||
}
|
||
}
|
||
|
||
.skin-box{
|
||
|
||
margin-top: 50px;
|
||
.avatar{
|
||
position: relative;
|
||
margin: 0 auto;
|
||
width: 336px;
|
||
height: 336px;
|
||
background: url('@/assets/images/avatar.png') no-repeat center center / cover;
|
||
.item{
|
||
top: 96px;
|
||
left: -109px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
position: absolute;
|
||
padding: 9px 27px;
|
||
height: 50px;
|
||
font-size: 24px;
|
||
color:rgba(0,0,0,0.65);
|
||
background: #fff;
|
||
border-radius: 25px 25px 0 25px;
|
||
white-space: nowrap;
|
||
img{
|
||
width: 44px;
|
||
height: 20px;
|
||
margin-left: 8px;
|
||
}
|
||
}
|
||
.item2{
|
||
top: 16px;
|
||
left: 244px;
|
||
}
|
||
.item3{
|
||
top: 160px;
|
||
left: 293px;
|
||
}
|
||
}
|
||
.legend{
|
||
margin: 30px auto 0;
|
||
width: 531px;
|
||
height: 30px;
|
||
background: url('@/assets/images/legend.png') no-repeat center center / cover;
|
||
}
|
||
}
|
||
.text{
|
||
margin-top: 36px;
|
||
font-size: 30px;
|
||
color: #000;
|
||
line-height: 48px;
|
||
}
|
||
|
||
|
||
}
|
||
.tips{
|
||
margin-top: 20px;
|
||
// height: 254px;
|
||
// background: url('@/assets/images/content5-tips.png') no-repeat center center / cover;
|
||
}
|
||
|
||
}
|
||
}
|
||
</style> |