334 lines
12 KiB
TypeScript
334 lines
12 KiB
TypeScript
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>
|
||
);
|
||
} |