Files
app_03_pregnancy/frontend/src/app/pages/ExpertConsultationPage.tsx
Karlcarlo 409dabafb5 feat: 初始化孕育无忧全栈项目基础代码结构
完成项目初始搭建,包含后端FastAPI+MongoDB服务、前端React+Vite项目的完整基础结构,添加各类配置文件与依赖清单,涵盖核心业务初始框架。
2026-05-21 00:46:48 +08:00

334 lines
12 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}