添加后端API和前端项目的初始目录结构,包括应用层模块、工具模块和模型定义 配置前后端的.dockerignore文件以排除开发环境文件 添加前端样式文件,包括字体、Tailwind CSS和主题配置 初始化Python虚拟环境中的类型标记文件
624 lines
28 KiB
TypeScript
624 lines
28 KiB
TypeScript
import { useState, useEffect } from "react";
|
||
import { useNavigate } from "react-router";
|
||
import { motion } from "motion/react";
|
||
import {
|
||
ChevronLeft,
|
||
TrendingUp,
|
||
Info,
|
||
Download,
|
||
Share2,
|
||
Loader2,
|
||
Heart,
|
||
} from "lucide-react";
|
||
import { Button } from "../components/ui/button";
|
||
import { Card } from "../components/ui/card";
|
||
import { Badge } from "../components/ui/badge";
|
||
import {
|
||
LineChart,
|
||
Line,
|
||
XAxis,
|
||
YAxis,
|
||
CartesianGrid,
|
||
Tooltip,
|
||
Legend,
|
||
ResponsiveContainer,
|
||
Area,
|
||
AreaChart,
|
||
} from "recharts";
|
||
import { getGrowthCurve } from "../services/api";
|
||
import { useUser } from "../context/UserContext";
|
||
|
||
// 宝宝预期大小数据(按孕周,基于WHO标准)
|
||
// 4-19周:身长为头臀长(CRL);20周起:身长为头到脚全身长
|
||
const BABY_EXPECTED_DATA: Record<number, { emoji: string; fruit: string; length: string; weight: string; highlight: string }> = {
|
||
4: { emoji: "🫘", fruit: "罂粟种子", length: "0.1cm", weight: "<1g", highlight: "心脏开始跳动" },
|
||
5: { emoji: "🫘", fruit: "芝麻粒", length: "0.2cm", weight: "<1g", highlight: "神经管开始发育" },
|
||
6: { emoji: "🫘", fruit: "小扁豆", length: "0.6cm", weight: "<1g", highlight: "小手小脚在萌芽" },
|
||
7: { emoji: "🫐", fruit: "蓝莓", length: "1.3cm", weight: "<1g", highlight: "五官开始形成" },
|
||
8: { emoji: "🫐", fruit: "覆盆子", length: "1.6cm", weight: "1g", highlight: "手指脚趾开始分化" },
|
||
9: { emoji: "🍇", fruit: "葡萄", length: "2.3cm", weight: "2g", highlight: "可以微微活动了" },
|
||
10: { emoji: "🍊", fruit: "金橘", length: "3.1cm", weight: "4g", highlight: "所有器官已初步形成" },
|
||
11: { emoji: "🫒", fruit: "无花果", length: "4.1cm", weight: "7g", highlight: "能做出吞咽动作" },
|
||
12: { emoji: "🍋", fruit: "青柠", length: "5.4cm", weight: "14g", highlight: "进入稳定的孕中期" },
|
||
13: { emoji: "🍋", fruit: "柠檬", length: "7.4cm", weight: "23g", highlight: "指纹开始形成" },
|
||
14: { emoji: "🍑", fruit: "桃子", length: "8.7cm", weight: "43g", highlight: "能做出表情了" },
|
||
16: { emoji: "🥑", fruit: "牛油果", length: "11.6cm", weight: "100g", highlight: "开始能听到声音" },
|
||
18: { emoji: "🫑", fruit: "甜椒", length: "14.2cm", weight: "190g", highlight: "妈妈可能感到胎动了" },
|
||
20: { emoji: "🍌", fruit: "香蕉", length: "25.6cm", weight: "300g", highlight: "味觉开始发育" },
|
||
22: { emoji: "🥭", fruit: "木瓜", length: "27.8cm", weight: "430g", highlight: "眉毛和眼睫毛长出" },
|
||
24: { emoji: "🌽", fruit: "玉米", length: "30cm", weight: "600g", highlight: "肺部开始发育" },
|
||
26: { emoji: "🥦", fruit: "菜花", length: "35.6cm", weight: "760g", highlight: "能睁开眼睛了" },
|
||
28: { emoji: "🍆", fruit: "茄子", length: "37.6cm", weight: "1000g", highlight: "开始有规律的作息" },
|
||
30: { emoji: "🥬", fruit: "卷心菜", length: "39.9cm", weight: "1320g", highlight: "大脑快速发育中" },
|
||
32: { emoji: "🥥", fruit: "椰子", length: "42.4cm", weight: "1700g", highlight: "皮下脂肪增多" },
|
||
34: { emoji: "🍈", fruit: "哈密瓜", length: "45cm", weight: "2150g", highlight: "肺部接近成熟" },
|
||
36: { emoji: "🍈", fruit: "蜜瓜", length: "47.4cm", weight: "2620g", highlight: "头部开始入盆" },
|
||
38: { emoji: "🎃", fruit: "南瓜", length: "49.8cm", weight: "3080g", highlight: "各器官发育成熟" },
|
||
40: { emoji: "🍉", fruit: "小西瓜", length: "51.2cm", weight: "3460g", highlight: "随时准备来到这个世界" },
|
||
};
|
||
|
||
function getBabyExpectedData(week: number) {
|
||
const weeks = Object.keys(BABY_EXPECTED_DATA).map(Number).sort((a, b) => a - b);
|
||
let closest = weeks[0];
|
||
for (const w of weeks) {
|
||
if (w <= week) closest = w;
|
||
else break;
|
||
}
|
||
return BABY_EXPECTED_DATA[closest];
|
||
}
|
||
|
||
// 生成孕周里程碑时间线数据
|
||
function getGrowthMilestones(currentWeek: number) {
|
||
const milestones = [
|
||
{ week: 8, label: "心跳可检测", emoji: "💓" },
|
||
{ week: 12, label: "首次B超", emoji: "🔍" },
|
||
{ week: 16, label: "开始感知声音", emoji: "👂" },
|
||
{ week: 20, label: "大排畸检查", emoji: "🏥" },
|
||
{ week: 24, label: "肺部发育", emoji: "🫁" },
|
||
{ week: 28, label: "眼睛能睁开", emoji: "👀" },
|
||
{ week: 32, label: "快速增重", emoji: "📈" },
|
||
{ week: 36, label: "接近足月", emoji: "🎯" },
|
||
{ week: 40, label: "预产期到来", emoji: "🎉" },
|
||
];
|
||
return milestones.map(m => ({ ...m, passed: currentWeek >= m.week }));
|
||
}
|
||
|
||
export function GrowthCurvePage() {
|
||
const navigate = useNavigate();
|
||
const { user } = useUser();
|
||
const currentWeek = user.currentWeek || 0;
|
||
const [selectedMetric, setSelectedMetric] = useState("bpd");
|
||
const [loading, setLoading] = useState(true);
|
||
|
||
// Standard reference data (no fake values)
|
||
const standardData: Record<string, { name: string; unit: string; description: string; data: { week: number; value?: number; standard: number; lower: number; upper: number }[] }> = {
|
||
bpd: {
|
||
name: "双顶径 (BPD)",
|
||
unit: "mm",
|
||
description: "胎儿头部左右两侧之间最宽部位的长度",
|
||
data: [
|
||
{ week: 12, standard: 20, lower: 18, upper: 22 },
|
||
{ week: 16, standard: 35, lower: 32, upper: 38 },
|
||
{ week: 20, standard: 47, lower: 43, upper: 51 },
|
||
{ week: 24, standard: 60, lower: 56, upper: 64 },
|
||
{ week: 28, standard: 71, lower: 67, upper: 75 },
|
||
{ week: 32, standard: 81, lower: 77, upper: 85 },
|
||
{ week: 36, standard: 89, lower: 85, upper: 93 },
|
||
],
|
||
},
|
||
ac: {
|
||
name: "腹围 (AC)",
|
||
unit: "mm",
|
||
description: "胎儿腹部一周的长度",
|
||
data: [
|
||
{ week: 12, standard: 60, lower: 54, upper: 66 },
|
||
{ week: 16, standard: 102, lower: 94, upper: 110 },
|
||
{ week: 20, standard: 148, lower: 138, upper: 158 },
|
||
{ week: 24, standard: 192, lower: 180, upper: 204 },
|
||
{ week: 28, standard: 235, lower: 221, upper: 249 },
|
||
{ week: 32, standard: 279, lower: 263, upper: 295 },
|
||
{ week: 36, standard: 318, lower: 300, upper: 336 },
|
||
],
|
||
},
|
||
fl: {
|
||
name: "股骨长 (FL)",
|
||
unit: "mm",
|
||
description: "胎儿大腿骨的长度",
|
||
data: [
|
||
{ week: 12, standard: 8, lower: 7, upper: 9 },
|
||
{ week: 16, standard: 22, lower: 20, upper: 24 },
|
||
{ week: 20, standard: 34, lower: 31, upper: 37 },
|
||
{ week: 24, standard: 45, lower: 42, upper: 48 },
|
||
{ week: 28, standard: 54, lower: 51, upper: 57 },
|
||
{ week: 32, standard: 63, lower: 60, upper: 66 },
|
||
{ week: 36, standard: 70, lower: 67, upper: 73 },
|
||
],
|
||
},
|
||
};
|
||
|
||
const [growthData, setGrowthData] = useState(standardData);
|
||
|
||
useEffect(() => {
|
||
getGrowthCurve()
|
||
.then((data) => {
|
||
if (data.data_points && data.data_points.length > 0) {
|
||
// Merge API data points into standard data
|
||
const updated = { ...standardData };
|
||
for (const metric of ["bpd", "ac", "fl"] as const) {
|
||
const apiPoints = data.data_points.filter((p) => p[metric] != null);
|
||
if (apiPoints.length > 0) {
|
||
updated[metric] = {
|
||
...updated[metric],
|
||
data: updated[metric].data.map((d) => {
|
||
const match = apiPoints.find((p) => p.week === d.week);
|
||
return match && match[metric] != null
|
||
? { ...d, value: match[metric]! }
|
||
: d;
|
||
}),
|
||
};
|
||
}
|
||
}
|
||
setGrowthData(updated);
|
||
}
|
||
})
|
||
.catch(() => {/* keep standard data */})
|
||
.finally(() => setLoading(false));
|
||
}, []);
|
||
|
||
const currentData = growthData[selectedMetric];
|
||
|
||
// 检查是否有真实数据
|
||
const hasRealData = currentData.data.some(d => d.value != null);
|
||
|
||
// 生成动态评估结果
|
||
const generateAssessment = () => {
|
||
if (!hasRealData) {
|
||
return "暂无生长数据。上传B超报告后,系统会自动记录宝宝的生长数据并生成评估结果。";
|
||
}
|
||
|
||
const realDataPoints = currentData.data.filter(d => d.value != null);
|
||
const latestPoint = realDataPoints[realDataPoints.length - 1];
|
||
|
||
if (!latestPoint) {
|
||
return "暂无生长数据。上传B超报告后,系统会自动记录宝宝的生长数据并生成评估结果。";
|
||
}
|
||
|
||
const { value, lower, upper, week, standard } = latestPoint;
|
||
const metricName = currentData.name.split(" ")[0];
|
||
|
||
if (value! >= lower && value! <= upper) {
|
||
const deviation = Math.abs(value! - standard);
|
||
const deviationPercent = ((deviation / standard) * 100).toFixed(1);
|
||
|
||
if (deviation < standard * 0.05) {
|
||
return `宝宝的${metricName}发育非常好!孕${week}周的测量值为${value}${currentData.unit},与标准值${standard}${currentData.unit}非常接近,在正常范围内(${lower}-${upper}${currentData.unit})。继续保持健康的生活方式,按时产检。`;
|
||
} else {
|
||
return `宝宝的${metricName}发育良好!孕${week}周的测量值为${value}${currentData.unit},在正常范围内(${lower}-${upper}${currentData.unit}),与标准值${standard}${currentData.unit}相差约${deviationPercent}%。继续保持健康的生活方式,按时产检。`;
|
||
}
|
||
} else if (value! < lower) {
|
||
const gap = lower - value!;
|
||
return `宝宝的${metricName}略偏小。孕${week}周的测量值为${value}${currentData.unit},低于正常范围下限(${lower}${currentData.unit})约${gap}${currentData.unit}。建议加强营养摄入,多吃富含蛋白质的食物,并及时咨询产科医生,进行进一步评估。`;
|
||
} else {
|
||
const gap = value! - upper;
|
||
return `宝宝的${metricName}略偏大。孕${week}周的测量值为${value}${currentData.unit},高于正常范围上限(${upper}${currentData.unit})约${gap}${currentData.unit}。建议控制饮食,避免高糖高脂食物,并及时咨询产科医生,进行进一步评估。`;
|
||
}
|
||
};
|
||
|
||
const CustomTooltip = ({ active, payload }: any) => {
|
||
if (active && payload && payload.length) {
|
||
return (
|
||
<div className="bg-white p-3 rounded-xl shadow-sm border border-gray-100">
|
||
<p className="font-semibold text-gray-800 mb-2">
|
||
孕{payload[0].payload.week}周
|
||
</p>
|
||
<p className="text-sm text-gray-500">
|
||
标准值: {payload[0].payload.standard} {currentData.unit}
|
||
</p>
|
||
<p className="text-xs text-gray-400 mt-1">
|
||
正常范围: {payload[0].payload.lower}-{payload[0].payload.upper}{" "}
|
||
{currentData.unit}
|
||
</p>
|
||
</div>
|
||
);
|
||
}
|
||
return null;
|
||
};
|
||
|
||
return (
|
||
<div className="min-h-dvh bg-morandi-50">
|
||
{/* 顶部导航 */}
|
||
<div className="bg-white border-b border-gray-100 sticky top-0 z-10">
|
||
<div className="mx-auto px-4 py-4 flex items-center justify-between">
|
||
<div className="flex items-center gap-3">
|
||
<button
|
||
onClick={() => navigate(-1)}
|
||
className="w-10 h-10 rounded-full hover:bg-gray-100 flex items-center justify-center"
|
||
>
|
||
<ChevronLeft className="w-6 h-6 text-gray-600" />
|
||
</button>
|
||
<div className="flex items-center gap-2">
|
||
<div className="w-10 h-10 bg-gradient-to-br from-morandi-400 to-morandi-400 rounded-full flex items-center justify-center">
|
||
<TrendingUp className="w-5 h-5 text-white" />
|
||
</div>
|
||
<div>
|
||
<h1 className="font-semibold text-gray-800">胎儿生长曲线</h1>
|
||
<p className="text-xs text-gray-500">基于B超数据自动生成</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="mx-auto p-4 space-y-6">
|
||
{/* 无B超数据时展示宝宝预期大小 */}
|
||
{!hasRealData && currentWeek > 0 && (() => {
|
||
const babyData = getBabyExpectedData(currentWeek);
|
||
const milestones = getGrowthMilestones(currentWeek);
|
||
const progress = Math.min(100, Math.max(0, ((currentWeek - 4) / 36) * 100));
|
||
if (!babyData) return null;
|
||
return (
|
||
<>
|
||
{/* 宝宝当前大小 */}
|
||
<motion.div
|
||
initial={{ opacity: 0, y: 20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
>
|
||
<Card className="bg-white rounded-2xl p-6 shadow-sm border-0 overflow-hidden relative">
|
||
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-bl from-morandi-100 to-transparent rounded-bl-full opacity-60" />
|
||
<div className="relative">
|
||
<h2 className="font-semibold text-gray-800 mb-4 flex items-center gap-2">
|
||
<Heart className="w-5 h-5 text-morandi-rose-400" />
|
||
宝宝现在有多大?
|
||
</h2>
|
||
<div className="text-center py-4">
|
||
<motion.div
|
||
initial={{ scale: 0.5, opacity: 0 }}
|
||
animate={{ scale: 1, opacity: 1 }}
|
||
transition={{ type: "spring", duration: 0.8 }}
|
||
className="text-7xl mb-4"
|
||
>
|
||
{babyData.emoji}
|
||
</motion.div>
|
||
<motion.p
|
||
initial={{ opacity: 0, y: 10 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ delay: 0.3 }}
|
||
className="text-lg text-gray-700 font-medium mb-1"
|
||
>
|
||
孕{currentWeek}周,宝宝像一个<span className="text-morandi-500 font-semibold">{babyData.fruit}</span>
|
||
</motion.p>
|
||
<p className="text-sm text-gray-400">{babyData.highlight}</p>
|
||
</div>
|
||
|
||
{/* 预期数据 */}
|
||
<div className="grid grid-cols-2 gap-3 mt-4">
|
||
<div className="bg-morandi-50 rounded-xl p-4 text-center">
|
||
<p className="text-xs text-gray-400 mb-1">预期身长</p>
|
||
<p className="text-xl font-semibold text-morandi-600">{babyData.length}</p>
|
||
</div>
|
||
<div className="bg-morandi-50 rounded-xl p-4 text-center">
|
||
<p className="text-xs text-gray-400 mb-1">预期体重</p>
|
||
<p className="text-xl font-semibold text-morandi-600">{babyData.weight}</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 孕期进度 */}
|
||
<div className="mt-5">
|
||
<div className="flex items-center justify-between mb-2">
|
||
<span className="text-xs text-gray-400">孕期进度</span>
|
||
<span className="text-xs text-morandi-500 font-medium">{Math.round(progress)}%</span>
|
||
</div>
|
||
<div className="h-2.5 bg-morandi-50 rounded-full overflow-hidden">
|
||
<motion.div
|
||
className="h-full bg-gradient-to-r from-morandi-300 to-morandi-500 rounded-full"
|
||
initial={{ width: 0 }}
|
||
animate={{ width: `${progress}%` }}
|
||
transition={{ duration: 1.2, ease: "easeOut" }}
|
||
/>
|
||
</div>
|
||
<div className="flex justify-between mt-1">
|
||
<span className="text-[10px] text-gray-300">4周</span>
|
||
<span className="text-[10px] text-gray-300">40周</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
</motion.div>
|
||
|
||
{/* 成长里程碑时间线 */}
|
||
<motion.div
|
||
initial={{ opacity: 0, y: 20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ delay: 0.15 }}
|
||
>
|
||
<Card className="bg-white rounded-2xl p-6 shadow-sm border-0">
|
||
<h3 className="font-semibold text-gray-800 mb-4">成长里程碑</h3>
|
||
<div className="space-y-0">
|
||
{milestones.map((m, i) => (
|
||
<div key={m.week} className="flex items-start gap-3">
|
||
{/* 时间线连接线 */}
|
||
<div className="flex flex-col items-center">
|
||
<div className={`w-8 h-8 rounded-full flex items-center justify-center text-sm flex-shrink-0 ${
|
||
m.passed
|
||
? "bg-morandi-400 text-white"
|
||
: currentWeek >= m.week - 2
|
||
? "bg-morandi-100 text-morandi-500 ring-2 ring-morandi-300"
|
||
: "bg-gray-100 text-gray-400"
|
||
}`}>
|
||
{m.emoji}
|
||
</div>
|
||
{i < milestones.length - 1 && (
|
||
<div className={`w-0.5 h-6 ${m.passed ? "bg-morandi-300" : "bg-gray-200"}`} />
|
||
)}
|
||
</div>
|
||
<div className="pt-1.5">
|
||
<p className={`text-sm font-medium ${m.passed ? "text-gray-700" : "text-gray-400"}`}>
|
||
{m.label}
|
||
</p>
|
||
<p className={`text-xs ${m.passed ? "text-morandi-500" : "text-gray-300"}`}>
|
||
孕{m.week}周
|
||
</p>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</Card>
|
||
</motion.div>
|
||
|
||
{/* 上传B超提示 */}
|
||
<motion.div
|
||
initial={{ opacity: 0, y: 20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ delay: 0.25 }}
|
||
>
|
||
<Card className="bg-gradient-to-r from-morandi-sage-50 to-morandi-50 border-morandi-200 rounded-2xl p-4">
|
||
<div className="flex items-start gap-3">
|
||
<Info className="w-5 h-5 text-morandi-sage-500 mt-0.5 flex-shrink-0" />
|
||
<div>
|
||
<h3 className="font-semibold text-gray-700 text-sm mb-1">查看精准发育数据</h3>
|
||
<p className="text-xs text-gray-500 leading-relaxed mb-3">
|
||
上传B超报告后,系统会自动提取胎儿双顶径、腹围、股骨长等数据,生成个性化的生长曲线,让你更直观地了解宝宝发育情况。
|
||
</p>
|
||
<Button
|
||
onClick={() => navigate("/camera")}
|
||
className="bg-gradient-to-r from-morandi-400 to-morandi-500"
|
||
size="sm"
|
||
>
|
||
上传B超报告
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
</motion.div>
|
||
</>
|
||
);
|
||
})()}
|
||
|
||
{/* 指标选择 - 只在有B超数据时显示 */}
|
||
{hasRealData && (
|
||
<motion.div
|
||
initial={{ opacity: 0, y: 20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
>
|
||
<div className="flex gap-3 overflow-x-auto pb-2">
|
||
<button
|
||
onClick={() => setSelectedMetric("bpd")}
|
||
className={`flex-shrink-0 px-6 py-3 rounded-2xl font-semibold text-sm transition-all ${
|
||
selectedMetric === "bpd"
|
||
? "bg-gradient-to-r from-morandi-400 to-morandi-400 text-white shadow-sm"
|
||
: "bg-white text-gray-600 hover:bg-gray-50"
|
||
}`}
|
||
>
|
||
双顶径
|
||
</button>
|
||
<button
|
||
onClick={() => setSelectedMetric("ac")}
|
||
className={`flex-shrink-0 px-6 py-3 rounded-2xl font-semibold text-sm transition-all ${
|
||
selectedMetric === "ac"
|
||
? "bg-gradient-to-r from-morandi-400 to-morandi-400 text-white shadow-sm"
|
||
: "bg-white text-gray-600 hover:bg-gray-50"
|
||
}`}
|
||
>
|
||
腹围
|
||
</button>
|
||
<button
|
||
onClick={() => setSelectedMetric("fl")}
|
||
className={`flex-shrink-0 px-6 py-3 rounded-2xl font-semibold text-sm transition-all ${
|
||
selectedMetric === "fl"
|
||
? "bg-gradient-to-r from-morandi-rose-400 to-morandi-rose-300 text-white shadow-sm"
|
||
: "bg-white text-gray-600 hover:bg-gray-50"
|
||
}`}
|
||
>
|
||
股骨长
|
||
</button>
|
||
</div>
|
||
</motion.div>
|
||
)}
|
||
|
||
{/* 有B超数据时显示指标详情、曲线图、数据表格 */}
|
||
{hasRealData && (
|
||
<>
|
||
{/* 指标说明 */}
|
||
<motion.div
|
||
initial={{ opacity: 0, y: 20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ delay: 0.1 }}
|
||
>
|
||
<Card className="bg-morandi-slate-50 border-morandi-200 rounded-2xl p-4">
|
||
<div className="flex items-start gap-3">
|
||
<Info className="w-5 h-5 text-morandi-slate-500 mt-0.5 flex-shrink-0" />
|
||
<div>
|
||
<h3 className="font-semibold text-gray-800 mb-1">
|
||
{currentData.name}
|
||
</h3>
|
||
<p className="text-sm text-gray-600">
|
||
{currentData.description}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
</motion.div>
|
||
|
||
{/* 生长曲线图 */}
|
||
<motion.div
|
||
initial={{ opacity: 0, y: 20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ delay: 0.2 }}
|
||
>
|
||
<Card className="bg-white rounded-2xl p-6 shadow-sm border-0">
|
||
<div className="flex items-center justify-between mb-6">
|
||
<h2 className="font-semibold text-gray-800">生长趋势</h2>
|
||
<div className="flex items-center gap-4 text-sm">
|
||
<div className="flex items-center gap-2">
|
||
<div className="w-3 h-3 bg-gray-300 rounded-full"></div>
|
||
<span className="text-gray-600">标准值</span>
|
||
</div>
|
||
<div className="flex items-center gap-2">
|
||
<div className="w-3 h-3 bg-morandi-slate-200 rounded-full"></div>
|
||
<span className="text-gray-600">正常范围</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<ResponsiveContainer width="100%" height={300}>
|
||
<AreaChart data={currentData.data}>
|
||
<defs>
|
||
<linearGradient id="colorRange" x1="0" y1="0" x2="0" y2="1">
|
||
<stop offset="5%" stopColor="#93c5fd" stopOpacity={0.3} />
|
||
<stop offset="95%" stopColor="#93c5fd" stopOpacity={0.1} />
|
||
</linearGradient>
|
||
</defs>
|
||
<CartesianGrid strokeDasharray="3 3" stroke="#e5e7eb" />
|
||
<XAxis
|
||
dataKey="week"
|
||
label={{ value: "孕周", position: "insideBottom", offset: -5 }}
|
||
stroke="#9ca3af"
|
||
/>
|
||
<YAxis
|
||
label={{
|
||
value: `${currentData.unit}`,
|
||
angle: -90,
|
||
position: "insideLeft",
|
||
}}
|
||
stroke="#9ca3af"
|
||
/>
|
||
<Tooltip content={<CustomTooltip />} />
|
||
<Area
|
||
type="monotone"
|
||
dataKey="upper"
|
||
stroke="none"
|
||
fill="url(#colorRange)"
|
||
/>
|
||
<Area
|
||
type="monotone"
|
||
dataKey="lower"
|
||
stroke="none"
|
||
fill="#ffffff"
|
||
/>
|
||
<Line
|
||
type="monotone"
|
||
dataKey="standard"
|
||
stroke="#d1d5db"
|
||
strokeWidth={2}
|
||
dot={false}
|
||
strokeDasharray="5 5"
|
||
/>
|
||
{/* 完全移除实际值线条 */}
|
||
</AreaChart>
|
||
</ResponsiveContainer>
|
||
</Card>
|
||
</motion.div>
|
||
|
||
{/* 数据表格 */}
|
||
<motion.div
|
||
initial={{ opacity: 0, y: 20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ delay: 0.3 }}
|
||
>
|
||
<Card className="bg-white rounded-2xl p-6 shadow-sm border-0">
|
||
<h3 className="font-semibold text-gray-800 mb-4">详细数据</h3>
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full">
|
||
<thead>
|
||
<tr className="border-b border-gray-200">
|
||
<th className="text-left py-3 text-sm font-semibold text-gray-600">
|
||
孕周
|
||
</th>
|
||
<th className="text-center py-3 text-sm font-semibold text-gray-600">
|
||
实际值
|
||
</th>
|
||
<th className="text-center py-3 text-sm font-semibold text-gray-600">
|
||
标准值
|
||
</th>
|
||
<th className="text-center py-3 text-sm font-semibold text-gray-600">
|
||
正常范围
|
||
</th>
|
||
<th className="text-center py-3 text-sm font-semibold text-gray-600">
|
||
状态
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{currentData.data.map((row, index) => {
|
||
const isNormal =
|
||
row.value != null && row.value >= row.lower && row.value <= row.upper;
|
||
return (
|
||
<tr key={index} className="border-b border-gray-100">
|
||
<td className="py-3 text-sm text-gray-700">
|
||
{row.week}周
|
||
</td>
|
||
<td className="text-center py-3 text-sm font-semibold text-morandi-500">
|
||
{row.value != null ? `${row.value} ${currentData.unit}` : '-'}
|
||
</td>
|
||
<td className="text-center py-3 text-sm text-gray-600">
|
||
{row.standard} {currentData.unit}
|
||
</td>
|
||
<td className="text-center py-3 text-sm text-gray-500">
|
||
{row.lower}-{row.upper} {currentData.unit}
|
||
</td>
|
||
<td className="text-center py-3">
|
||
{row.value != null ? (
|
||
<Badge
|
||
className={`${
|
||
isNormal
|
||
? "bg-morandi-sage-100 text-morandi-sage-600"
|
||
: "bg-morandi-rose-100 text-morandi-600"
|
||
} hover:bg-current`}
|
||
>
|
||
{isNormal ? "正常" : "关注"}
|
||
</Badge>
|
||
) : (
|
||
<span className="text-gray-400 text-xs">-</span>
|
||
)}
|
||
</td>
|
||
</tr>
|
||
);
|
||
})}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</Card>
|
||
</motion.div>
|
||
|
||
{/* 医生建议 */}
|
||
<motion.div
|
||
initial={{ opacity: 0, y: 20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ delay: 0.4 }}
|
||
>
|
||
<Card className="bg-gradient-to-r from-morandi-sage-100 to-morandi-sage-100 border-morandi-sage-200 rounded-2xl p-4">
|
||
<h3 className="font-semibold text-gray-800 mb-2 flex items-center gap-2">
|
||
<TrendingUp className="w-5 h-5 text-morandi-sage-500" />
|
||
评估结果
|
||
</h3>
|
||
<p className="text-sm text-gray-700 leading-relaxed">
|
||
{generateAssessment()}
|
||
</p>
|
||
</Card>
|
||
</motion.div>
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
} |