添加后端API和前端项目的初始目录结构,包括应用层模块、工具模块和模型定义 配置前后端的.dockerignore文件以排除开发环境文件 添加前端样式文件,包括字体、Tailwind CSS和主题配置 初始化Python虚拟环境中的类型标记文件
284 lines
10 KiB
TypeScript
284 lines
10 KiB
TypeScript
import { useState, useEffect } from "react";
|
||
import { useNavigate, useParams } from "react-router";
|
||
import { motion } from "motion/react";
|
||
import {
|
||
ChevronLeft,
|
||
Copy,
|
||
Check,
|
||
MessageCircle,
|
||
Clipboard,
|
||
Share2,
|
||
Download,
|
||
Loader2,
|
||
} from "lucide-react";
|
||
import { Button } from "../components/ui/button";
|
||
import { Card } from "../components/ui/card";
|
||
import { Badge } from "../components/ui/badge";
|
||
import { getDoctorQuestions } from "../services/api";
|
||
|
||
interface Question {
|
||
id: string;
|
||
category: string;
|
||
question: string;
|
||
reason: string;
|
||
priority: "high" | "medium" | "low";
|
||
}
|
||
|
||
export function DoctorQuestionsPage() {
|
||
const navigate = useNavigate();
|
||
const { reportId } = useParams();
|
||
const [copiedId, setCopiedId] = useState(null);
|
||
const [questions, setQuestions] = useState<Question[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
|
||
useEffect(() => {
|
||
if (!reportId) return;
|
||
getDoctorQuestions(reportId)
|
||
.then((data) => {
|
||
setQuestions(
|
||
data.questions.map((q, i) => ({
|
||
id: String(i),
|
||
category: q.category,
|
||
question: q.question,
|
||
reason: q.reason,
|
||
priority: (i < 2 ? "high" : i < 4 ? "medium" : "low") as Question["priority"],
|
||
}))
|
||
);
|
||
})
|
||
.catch(() => {/* keep empty */})
|
||
.finally(() => setLoading(false));
|
||
}, [reportId]);
|
||
|
||
const handleCopy = (text: string, id: string) => {
|
||
navigator.clipboard.writeText(text);
|
||
setCopiedId(id);
|
||
setTimeout(() => setCopiedId(null), 2000);
|
||
};
|
||
|
||
const handleCopyAll = () => {
|
||
const allQuestions = questions
|
||
.map((q, index) => `${index + 1}. ${q.question}`)
|
||
.join("\n\n");
|
||
navigator.clipboard.writeText(allQuestions);
|
||
setCopiedId("all");
|
||
setTimeout(() => setCopiedId(null), 2000);
|
||
};
|
||
|
||
const priorityLabels = {
|
||
high: { text: "重要", color: "bg-red-100 text-red-700" },
|
||
medium: { text: "建议", color: "bg-morandi-rose-100 text-morandi-rose-400" },
|
||
low: { text: "了解", color: "bg-morandi-50 text-morandi-500" },
|
||
};
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="min-h-dvh bg-morandi-50 flex items-center justify-center">
|
||
<div className="text-center">
|
||
<Loader2 className="w-8 h-8 text-morandi-400 animate-spin mx-auto mb-3" />
|
||
<p className="text-sm text-gray-500">正在生成问诊清单...</p>
|
||
</div>
|
||
</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">
|
||
<MessageCircle className="w-5 h-5 text-white" />
|
||
</div>
|
||
<div>
|
||
<h1 className="font-semibold text-gray-800">问诊清单</h1>
|
||
<p className="text-xs text-gray-500">
|
||
智能生成{questions.length}个问题
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="flex gap-2">
|
||
<Button
|
||
onClick={handleCopyAll}
|
||
variant="outline"
|
||
size="sm"
|
||
className="rounded-full"
|
||
>
|
||
{copiedId === "all" ? (
|
||
<>
|
||
<Check className="w-4 h-4 mr-2" />
|
||
已复制
|
||
</>
|
||
) : (
|
||
<>
|
||
<Clipboard className="w-4 h-4 mr-2" />
|
||
全部复制
|
||
</>
|
||
)}
|
||
</Button>
|
||
</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>✓ 点击单个问题可复制,方便向医生提问</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="space-y-4">
|
||
{questions.map((question, index) => (
|
||
<motion.div
|
||
key={question.id}
|
||
initial={{ opacity: 0, x: -20 }}
|
||
animate={{ opacity: 1, x: 0 }}
|
||
transition={{ delay: 0.2 + index * 0.05 }}
|
||
>
|
||
<Card className="bg-white rounded-2xl p-5 shadow-sm border-0 hover:shadow-md transition-shadow">
|
||
<div className="flex items-start gap-4">
|
||
<div className="flex-shrink-0 w-8 h-8 bg-morandi-100 text-morandi-500 rounded-full flex items-center justify-center font-semibold text-sm">
|
||
{index + 1}
|
||
</div>
|
||
|
||
<div className="flex-1 min-w-0">
|
||
<div className="flex items-center gap-2 mb-3">
|
||
<Badge
|
||
className={`${
|
||
priorityLabels[question.priority].color
|
||
} hover:bg-current`}
|
||
>
|
||
{priorityLabels[question.priority].text}
|
||
</Badge>
|
||
<span className="text-xs text-gray-500">
|
||
{question.category}
|
||
</span>
|
||
</div>
|
||
|
||
<p className="text-gray-800 leading-relaxed mb-3 font-medium">
|
||
{question.question}
|
||
</p>
|
||
|
||
<div className="bg-morandi-slate-50 rounded-xl p-3 mb-3">
|
||
<p className="text-sm text-morandi-500">
|
||
💡 <strong>为什么问:</strong>
|
||
{question.reason}
|
||
</p>
|
||
</div>
|
||
|
||
<Button
|
||
onClick={() =>
|
||
handleCopy(question.question, question.id)
|
||
}
|
||
size="sm"
|
||
variant="outline"
|
||
className="w-full"
|
||
>
|
||
{copiedId === question.id ? (
|
||
<>
|
||
<Check className="w-4 h-4 mr-2" />
|
||
已复制
|
||
</>
|
||
) : (
|
||
<>
|
||
<Copy className="w-4 h-4 mr-2" />
|
||
复制此问题
|
||
</>
|
||
)}
|
||
</Button>
|
||
</div>
|
||
</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-3 flex items-center gap-2">
|
||
<MessageCircle className="w-5 h-5 text-morandi-400" />
|
||
💡 就诊小贴士
|
||
</h3>
|
||
<ul className="space-y-2 text-sm text-gray-600">
|
||
<li className="flex items-start gap-2">
|
||
<span className="flex-shrink-0 w-1.5 h-1.5 bg-morandi-400 rounded-full mt-2"></span>
|
||
<span>
|
||
问诊时可以拿出手机查看问题清单,避免紧张时忘记要问的内容
|
||
</span>
|
||
</li>
|
||
<li className="flex items-start gap-2">
|
||
<span className="flex-shrink-0 w-1.5 h-1.5 bg-morandi-400 rounded-full mt-2"></span>
|
||
<span>听从医生建议,记录重要的用药和注意事项</span>
|
||
</li>
|
||
<li className="flex items-start gap-2">
|
||
<span className="flex-shrink-0 w-1.5 h-1.5 bg-morandi-400 rounded-full mt-2"></span>
|
||
<span>如医生时间有限,优先询问标记为"重要"的问题</span>
|
||
</li>
|
||
<li className="flex items-start gap-2">
|
||
<span className="flex-shrink-0 w-1.5 h-1.5 bg-morandi-400 rounded-full mt-2"></span>
|
||
<span>
|
||
就诊后可返回APP查看AI建议,结合医生意见调整生活方式
|
||
</span>
|
||
</li>
|
||
</ul>
|
||
</Card>
|
||
</motion.div>
|
||
|
||
{/* 底部操作 */}
|
||
<motion.div
|
||
initial={{ opacity: 0, y: 20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ delay: 0.6 }}
|
||
>
|
||
<div className="grid grid-cols-2 gap-3">
|
||
<Button
|
||
variant="outline"
|
||
className="h-12"
|
||
onClick={() => navigate(`/share/${reportId}`)}
|
||
>
|
||
<Share2 className="w-4 h-4 mr-2" />
|
||
分享清单
|
||
</Button>
|
||
<Button className="h-12 bg-morandi-400">
|
||
<Download className="w-4 h-4 mr-2" />
|
||
导出PDF
|
||
</Button>
|
||
</div>
|
||
</motion.div>
|
||
</div>
|
||
</div>
|
||
);
|
||
} |