chore: 初始化项目结构并添加基础配置文件
添加后端API和前端项目的初始目录结构,包括应用层模块、工具模块和模型定义 配置前后端的.dockerignore文件以排除开发环境文件 添加前端样式文件,包括字体、Tailwind CSS和主题配置 初始化Python虚拟环境中的类型标记文件
This commit is contained in:
624
前端-孕期报告解读-figma/src/app/pages/GrowthCurvePage.tsx
Normal file
624
前端-孕期报告解读-figma/src/app/pages/GrowthCurvePage.tsx
Normal file
@@ -0,0 +1,624 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user