feat: 初始化孕育无忧全栈项目基础代码结构

完成项目初始搭建,包含后端FastAPI+MongoDB服务、前端React+Vite项目的完整基础结构,添加各类配置文件与依赖清单,涵盖核心业务初始框架。
This commit is contained in:
2026-05-21 00:46:48 +08:00
commit 409dabafb5
142 changed files with 31010 additions and 0 deletions

View File

@@ -0,0 +1,334 @@
import { useState, useEffect } from "react";
import { useNavigate } from "react-router";
import { motion } from "motion/react";
import {
ChevronLeft,
UserCheck,
Star,
Clock,
MessageCircle,
Video,
FileText,
Award,
ThumbsUp,
ChevronRight,
Loader2,
} from "lucide-react";
import { Button } from "../components/ui/button";
import { Card } from "../components/ui/card";
import { Badge } from "../components/ui/badge";
import { getExperts, type Doctor as DoctorDTO } from "../services/api";
interface Doctor {
id: string;
name: string;
title: string;
hospital: string;
specialty: string;
experience: string;
rating: number;
reviewCount: number;
consultCount: number;
responseTime: string;
price: number;
avatar: string;
tags: string[];
}
export function ExpertConsultationPage() {
const navigate = useNavigate();
const [selectedService, setSelectedService] = useState("text");
const [doctors, setDoctors] = useState<Doctor[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
getExperts()
.then((data) => {
setDoctors(
data.doctors.map((d) => ({
id: d.id,
name: d.name,
title: d.title,
hospital: d.hospital,
specialty: d.specialty,
experience: d.experience,
rating: d.rating,
reviewCount: d.review_count,
consultCount: d.consult_count,
responseTime: d.response_time,
price: d.price?.text || 50,
avatar: "",
tags: d.tags,
}))
);
})
.catch(() => {/* keep empty */})
.finally(() => setLoading(false));
}, []);
const services = [
{
id: "text",
name: "图文咨询",
icon: MessageCircle,
price: 50,
time: "24小时内回复",
description: "上传报告,医生详细解读",
},
{
id: "video",
name: "视频问诊",
icon: Video,
price: 200,
time: "预约时间",
description: "面对面沟通,更加详细",
},
{
id: "urgent",
name: "加急咨询",
icon: Clock,
price: 100,
time: "2小时内回复",
description: "紧急情况,优先处理",
},
];
if (loading) {
return (
<div className="min-h-dvh bg-morandi-50 flex items-center justify-center">
<Loader2 className="w-8 h-8 text-morandi-400 animate-spin" />
</div>
);
}
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-morandi-400 rounded-full flex items-center justify-center">
<UserCheck className="w-5 h-5 text-white" />
</div>
<div>
<h1 className="font-semibold text-gray-800">专家复核</h1>
<p className="text-xs text-gray-500">专业医生在线解答</p>
</div>
</div>
</div>
</div>
</div>
<div className="mx-auto p-4 space-y-6">
{/* 服务介绍 */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
>
<Card className="bg-gradient-to-br from-morandi-400 to-morandi-500 border-0 text-white p-6 rounded-3xl shadow-sm">
<h2 className="text-xl font-semibold mb-3">👨‍⚕️ 为什么需要专家复核</h2>
<div className="space-y-2 text-sm text-white/90">
<p>✓ AI解读仅供参考,复杂情况需专业医生判断</p>
<p>✓ 三甲医院主任医师,20年+临床经验</p>
<p>✓ 一对一解答,针对您的具体情况给出建议</p>
<p>✓ 支持图文、视频多种咨询方式</p>
</div>
</Card>
</motion.div>
{/* 咨询方式选择 */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
>
<h2 className="text-lg font-semibold text-gray-800 mb-4">
选择咨询方式
</h2>
<div className="grid grid-cols-3 gap-3">
{services.map((service) => (
<button
key={service.id}
onClick={() => setSelectedService(service.id as any)}
className={`p-4 rounded-2xl transition-all ${
selectedService === service.id
? "bg-morandi-400 text-white shadow-sm scale-105"
: "bg-white text-gray-600 hover:bg-gray-50"
}`}
>
<service.icon
className={`w-8 h-8 mx-auto mb-2 ${
selectedService === service.id
? "text-white"
: "text-morandi-400"
}`}
/>
<div className="font-semibold text-sm mb-1">
{service.name}
</div>
<div
className={`text-xs mb-2 ${
selectedService === service.id
? "text-white/80"
: "text-gray-500"
}`}
>
{service.time}
</div>
<div className="font-semibold">¥{service.price}</div>
</button>
))}
</div>
</motion.div>
{/* 推荐医生 */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
<h2 className="text-lg font-semibold text-gray-800 mb-4">
🏥 推荐专家
</h2>
<div className="space-y-4">
{doctors.map((doctor, index) => (
<motion.div
key={doctor.id}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.3 + index * 0.05 }}
>
<Card className="bg-white rounded-2xl p-5 shadow-sm hover:shadow-md transition-all border-0">
<div className="flex items-start gap-4 mb-4">
{/* 医生头像 */}
<div className="w-16 h-16 bg-morandi-400 rounded-full flex items-center justify-center text-white text-2xl font-semibold flex-shrink-0">
{doctor.name.charAt(0)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-2">
<h3 className="font-semibold text-gray-800 text-lg">
{doctor.name}
</h3>
<Badge className="bg-morandi-50 text-morandi-500 hover:bg-morandi-50">
{doctor.title}
</Badge>
{doctor.rating >= 4.9 && (
<Award className="w-4 h-4 text-yellow-500" />
)}
</div>
<div className="space-y-1 text-sm text-gray-600 mb-3">
<p>{doctor.hospital} · {doctor.specialty}</p>
<p>{doctor.experience}</p>
</div>
<div className="flex items-center gap-4 text-sm text-gray-500 mb-3">
<div className="flex items-center gap-1">
<Star className="w-4 h-4 text-yellow-500 fill-yellow-500" />
<span className="font-semibold text-gray-800">
{doctor.rating}
</span>
<span>({doctor.reviewCount}评价)</span>
</div>
<div className="flex items-center gap-1">
<ThumbsUp className="w-4 h-4" />
<span>{doctor.consultCount}次咨询</span>
</div>
</div>
<div className="flex flex-wrap gap-2 mb-3">
{doctor.tags.map((tag, i) => (
<span
key={i}
className="px-3 py-1 bg-morandi-50 text-morandi-500 text-xs rounded-full"
>
{tag}
</span>
))}
</div>
<div className="flex items-center gap-2 text-sm">
<Clock className="w-4 h-4 text-gray-400" />
<span className="text-gray-600">
响应时间:{doctor.responseTime}
</span>
</div>
</div>
</div>
<div className="flex items-center justify-between pt-4 border-t border-gray-100">
<div>
<span className="text-gray-500 text-sm mr-2">
咨询费用
</span>
<span className="text-2xl font-semibold text-morandi-500">
¥{doctor.price}
</span>
<span className="text-gray-500 text-sm">/次</span>
</div>
<Button className="bg-morandi-400 hover:bg-morandi-500">
立即咨询
<ChevronRight className="w-4 h-4 ml-1" />
</Button>
</div>
</Card>
</motion.div>
))}
</div>
</motion.div>
{/* 咨询流程 */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
>
<Card className="bg-white rounded-2xl p-5 shadow-sm border-0">
<h3 className="font-semibold text-gray-800 mb-4">📋 咨询流程</h3>
<div className="space-y-3">
{[
{ step: 1, text: "选择医生和咨询方式" },
{ step: 2, text: "上传检查报告和问题描述" },
{ step: 3, text: "支付咨询费用" },
{ step: 4, text: "医生接单并给出专业建议" },
{ step: 5, text: "可追问3次,获得满意答复" },
].map((item) => (
<div key={item.step} className="flex items-start gap-3">
<div className="flex-shrink-0 w-6 h-6 bg-morandi-100 text-morandi-500 rounded-full flex items-center justify-center text-sm font-semibold">
{item.step}
</div>
<p className="text-gray-700 pt-0.5">{item.text}</p>
</div>
))}
</div>
</Card>
</motion.div>
{/* 温馨提示 */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.6 }}
>
<Card className="bg-gradient-to-r from-morandi-rose-100 to-red-50 border-morandi-rose-300 rounded-2xl p-4">
<h3 className="font-semibold text-gray-800 mb-2">⚠️ 重要提示</h3>
<ul className="space-y-1 text-sm text-gray-600">
<li>• 在线咨询仅供参考,不能替代线下就诊</li>
<li>• 如有紧急情况,请立即前往医院就诊</li>
<li>• 咨询记录会保存,可随时查看</li>
<li>• 医生回复后24小时内可免费追问</li>
</ul>
</Card>
</motion.div>
</div>
</div>
);
}