feat: 初始化孕育无忧全栈项目基础代码结构
完成项目初始搭建,包含后端FastAPI+MongoDB服务、前端React+Vite项目的完整基础结构,添加各类配置文件与依赖清单,涵盖核心业务初始框架。
This commit is contained in:
334
frontend/src/app/pages/ExpertConsultationPage.tsx
Normal file
334
frontend/src/app/pages/ExpertConsultationPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user